diff --git a/CHANGELOG.md b/CHANGELOG.md
index efeeff8..ad9bc66 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
@@ -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
diff --git a/internal/guard/actionbarheight_test.go b/internal/guard/actionbarheight_test.go
index 4b79916..8f78256 100644
--- a/internal/guard/actionbarheight_test.go
+++ b/internal/guard/actionbarheight_test.go
@@ -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"
@@ -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)
+}
diff --git a/internal/guard/actionrail_test.go b/internal/guard/actionrail_test.go
index ca485b1..ab155da 100644
--- a/internal/guard/actionrail_test.go
+++ b/internal/guard/actionrail_test.go
@@ -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)
diff --git a/internal/guard/boxwidth_test.go b/internal/guard/boxwidth_test.go
index 91f08d8..66a4ade 100644
--- a/internal/guard/boxwidth_test.go
+++ b/internal/guard/boxwidth_test.go
@@ -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)
@@ -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)
}
}
})
diff --git a/internal/guard/controlnames_test.go b/internal/guard/controlnames_test.go
index b0ea2b1..79f5dc2 100644
--- a/internal/guard/controlnames_test.go
+++ b/internal/guard/controlnames_test.go
@@ -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, "+
diff --git a/internal/guard/detailpopup_test.go b/internal/guard/detailpopup_test.go
index d3363ca..0286c55 100644
--- a/internal/guard/detailpopup_test.go
+++ b/internal/guard/detailpopup_test.go
@@ -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
@@ -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.
diff --git a/internal/guard/foldhead_test.go b/internal/guard/foldhead_test.go
index 58d291c..766d19a 100644
--- a/internal/guard/foldhead_test.go
+++ b/internal/guard/foldhead_test.go
@@ -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)
}
}
diff --git a/internal/guard/formcolumns_test.go b/internal/guard/formcolumns_test.go
new file mode 100644
index 0000000..d3e74d3
--- /dev/null
+++ b/internal/guard/formcolumns_test.go
@@ -0,0 +1,355 @@
+package guard
+
+import (
+ "image/color"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/test"
+ "fyne.io/fyne/v2/theme"
+ "fyne.io/fyne/v2/widget"
+
+ "github.com/donislawdev/TestingFilesGenerator/internal/core"
+ "github.com/donislawdev/TestingFilesGenerator/internal/engine"
+ "github.com/donislawdev/TestingFilesGenerator/internal/format"
+ "github.com/donislawdev/TestingFilesGenerator/internal/gui/parts"
+ "github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
+ "github.com/donislawdev/TestingFilesGenerator/internal/gui/window"
+ "github.com/donislawdev/TestingFilesGenerator/internal/recipe"
+)
+
+// The layout of 2026-09-23 and what it promises, asked of the running
+// screens. Built round by round from the owner's verdicts on the real window.
+// docs/GUI-PROTOTYP-2026-09-23.md has the rounds, the measurements and what
+// was turned down.
+
+// TestShortFieldsShareARowAndTakeOneColumnEach asks the first row of the
+// single batch screen: the format, the size, how many and the damage stand in
+// one row, one column each, the same width - and the output directory, a path
+// nobody can predict the length of, takes more than one.
+//
+// The defect it keeps away is the one the owner reported twice: a box holding
+// "1" as wide as half the form, and a form one field tall per row.
+func TestShortFieldsShareARowAndTakeOneColumnEach(t *testing.T) {
+ ourTheme(t)
+ content, _ := laidOutWindow(t)
+ screen := tabContent(t, content, text.TabOneTarget())
+
+ labels := []string{text.FieldFormat(), text.FieldSize(), text.FieldCount(), text.FieldDamage()}
+ var first band
+ for i, label := range labels {
+ box, ok := objectBox(screen, controlUnder(screen, label))
+ if !ok {
+ t.Fatalf("the box under %q is not laid out", label)
+ }
+ if i == 0 {
+ first = box
+ continue
+ }
+ if off := box.Y - first.Y; off > 1 || off < -1 {
+ t.Errorf("%q stands at y=%.1f and %q at y=%.1f - the short fields of the first row are not in one row",
+ labels[0], first.Y, label, box.Y)
+ }
+ if off := box.Width - first.Width; off > 1 || off < -1 {
+ t.Errorf("%q is %.1f px wide and %q %.1f px - one column each is one width", labels[0], first.Width, label, box.Width)
+ }
+ if box.X <= first.X {
+ t.Errorf("%q starts at x=%.1f, not to the right of %q at x=%.1f", label, box.X, labels[0], first.X)
+ }
+ }
+ path, ok := objectBox(screen, entryUnder(t, screen, text.FieldOutputDir()))
+ if !ok {
+ t.Fatal("the output directory is not laid out")
+ }
+ if path.Width < first.Width*3 {
+ t.Errorf("the output directory is %.1f px wide, which is under three columns of %.1f - a path is the one "+
+ "value whose length nobody can predict", path.Width, first.Width)
+ }
+}
+
+// TestASizeBelowTheMinimumOffersTheSmallestSize presses the button the
+// refusal carries and asks what reaches the box.
+//
+// Reported by the owner from the running window: the refusal named the
+// smallest size a format can make as eight digits, and the only way to act on
+// it was to copy them by hand. Pressed rather than read, because a button
+// that says the right words and puts nothing in the box is the defect this is
+// about - and the box is asked afterwards whether the run still refuses it.
+func TestASizeBelowTheMinimumOffersTheSmallestSize(t *testing.T) {
+ host := newFakeHost(t)
+ screen := window.NewGenerate(host)
+ content := screen.Object()
+ fill(t, content, text.FieldSize(), "1")
+ screen.PressPreview()
+ join(host)
+
+ field := screen.Fields().Lookup(core.SettingSize)
+ if field == nil || field.Saying() == "" {
+ t.Fatal("a size of 1 B was not refused under the size box, so there is nothing to offer a fix for")
+ }
+ offered := field.Offered()
+ if offered == "" {
+ t.Fatalf("the size box says %q and offers no button to put it right", field.Saying())
+ }
+ press(t, content, offered)
+ join(host)
+
+ typed := entryUnder(t, content, text.FieldSize()).Text
+ bytes, err := core.ParseSize(typed)
+ if err != nil {
+ t.Fatalf("the button put %q in the size box, which is not a size: %v", typed, err)
+ }
+ if !strings.Contains(offered, core.ExactBytes(bytes)) {
+ t.Errorf("the button said %q and put %q in the box - what it says and what it does differ", offered, typed)
+ }
+ if said := field.Saying(); said != "" {
+ t.Errorf("the size the button put in the box is still refused: %q", said)
+ }
+}
+
+// TestALimitBelowTheMinimumOffersNoButtonThatLeadsBackToItself is the same
+// refusal under "Around a limit", where the box is not the size refused.
+//
+// The set is built one byte under the limit, on it and one over, so a limit
+// of 74 for a PNG refuses the file of 73. The button used to put the minimum,
+// 74, back in the box - the same 73, the same refusal, and a person pressing
+// it saw nothing change. Measured on 2026-09-23 after an outside review of
+// #126 (docs/REVIEW-126-2026-09-23.md). The smallest limit whose three files
+// all exist is the engine's to answer and it cannot yet, so the promise is
+// the modest one: this box gets the sentence and no button. The day the
+// engine answers, this guard is the one to rewrite into "one press, and the
+// limit is no longer refused".
+func TestALimitBelowTheMinimumOffersNoButtonThatLeadsBackToItself(t *testing.T) {
+ host := newFakeHost(t)
+ screen := window.NewRecipe(host)
+ body := screen.Object()
+ chooseFormat(t, body, "png")
+ chooseSizeWay(t, body, text.SizeWayBoundary())
+ fields := screen.Fields()
+ at := recipe.TargetAddress(1, recipe.KeyBoundary)
+ minimum := minimumOf(t, "png")
+ setBox(t, fields, at, strconv.FormatInt(minimum, 10))
+ pressNamed(t, body, text.ButtonPreview())
+ join(host)
+
+ field := fields.Lookup(at)
+ if field == nil {
+ t.Fatalf("the screen registers no box at %q", at)
+ }
+ if field.Saying() == "" {
+ t.Fatalf("a limit of %d B builds a file of %d B for a format whose smallest is %d B and was not refused, "+
+ "so there is nothing to offer a fix for.\n%s", minimum, minimum-1, minimum, allSaid(fields))
+ }
+ if offered := field.Offered(); offered != "" {
+ t.Errorf("the limit box offers %q under a refusal about the file one byte below the limit.\n"+
+ "Reason: the button puts the format's smallest size in the box, the box is the LIMIT, and\n"+
+ "the file under it is refused again - measured, the button led back to itself.", offered)
+ }
+}
+
+// minimumOf is the smallest file a format can make, asked of the registry.
+func minimumOf(t *testing.T, id string) int64 {
+ t.Helper()
+ d, err := format.Get(id)
+ if err != nil {
+ t.Fatalf("the registry has no %s: %v", id, err)
+ }
+ return d.MinBytes
+}
+
+// TestARefusalAboutWhatIsInTheDirectoryStandsUnderItAndOpensIt runs a
+// preview into a directory holding a manifest already.
+//
+// Three things the owner's report from the running window came to: the
+// refusal stands under the output directory box where it can be read whole,
+// it carries the way to that directory, and the line at the foot no longer
+// says it is still working out the cost of a run that was refused.
+func TestARefusalAboutWhatIsInTheDirectoryStandsUnderItAndOpensIt(t *testing.T) {
+ dir := t.TempDir()
+ if err := os.WriteFile(filepath.Join(dir, engine.DefaultManifestName), []byte("{}"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ host := newFakeHost(t)
+ screen := window.NewGenerate(host)
+ content := screen.Object()
+ fill(t, content, text.FieldSize(), "1kb")
+ entryUnder(t, content, text.FieldOutputDir()).SetText(dir)
+ screen.PressPreview()
+ join(host)
+
+ field := screen.Fields().Lookup(engine.SettingOutDir)
+ if field == nil {
+ t.Fatal("the screen registers no output directory field, so there is nothing to place the refusal under")
+ }
+ if !strings.Contains(field.Saying(), engine.DefaultManifestName) {
+ t.Fatalf("a manifest already in the directory was not refused under the output directory box - it says %q",
+ field.Saying())
+ }
+ if got := field.Offered(); got != text.ButtonOpenFolder() {
+ t.Fatalf("the refusal about the directory offers %q, not the way to the directory", got)
+ }
+ // The one on the screen: the bar keeps a button of the same name, hidden
+ // until a run has written something, and pressing that one is not the
+ // question.
+ shown := visibleButtonNamed(content, text.ButtonOpenFolder())
+ if shown == nil {
+ t.Fatalf("there is no %q button on the screen", text.ButtonOpenFolder())
+ }
+ shown.OnTapped()
+ if host.folderCount != 1 || host.folder != dir {
+ t.Errorf("the button opened %q %d time(s), and the directory the refusal is about is %q",
+ host.folder, host.folderCount, dir)
+ }
+ if said := shownText(content); strings.Contains(said, text.WorkingOutTheCost()) {
+ t.Errorf("the preview was refused and the screen still says %q", text.WorkingOutTheCost())
+ }
+}
+
+// TestARefusalStandsUnderItsRowAcrossIt asks where the sentence goes: under
+// the field it is about, starting on that field's edge, and reaching past the
+// field's own column - measured in the real window, the same sentence broke
+// into three lines inside a 185 px column and pushed the form down.
+func TestARefusalStandsUnderItsRowAcrossIt(t *testing.T) {
+ ourTheme(t)
+ generate := window.NewGenerate(newFakeHost(t))
+ screen := generate.Object()
+ w := test.NewTempWindow(t, screen)
+ w.Resize(fyne.NewSize(window.LargestOpening.Width, window.LargestOpening.Height))
+ fields := generate.Fields()
+ fields.Mark(core.SettingSize, errorString("a size of 3 B is below the smallest avif, which is 311 B"))
+ screen.Refresh()
+ w.Resize(fyne.NewSize(window.LargestOpening.Width, window.LargestOpening.Height+1))
+
+ box, ok := objectBox(screen, entryUnder(t, screen, text.FieldSize()))
+ if !ok {
+ t.Fatal("the size box is not laid out")
+ }
+ said, ok := labelBox(screen, fields.Lookup(core.SettingSize).Saying())
+ if !ok {
+ t.Fatal("the refusal under the size box is not on the screen")
+ }
+ if said.Y < box.Y+box.Height {
+ t.Errorf("the refusal starts at y=%.1f, above the bottom of its box at y=%.1f", said.Y, box.Y+box.Height)
+ }
+ if off := said.X - box.X; off > 1 || off < -1 {
+ t.Errorf("the refusal starts at x=%.1f and its box at x=%.1f - it belongs under the field it is about", said.X, box.X)
+ }
+ if said.Width <= box.Width {
+ t.Errorf("the refusal is %.1f px wide and its box %.1f - it is laid inside the field's column, not across the row",
+ said.Width, box.Width)
+ }
+}
+
+// TestAMenuShowsItsArrowInTheAccent asks the renderer of a menu what colour
+// its arrow is drawn in. The owner chose the accent from three variants shown
+// side by side, after a menu and a box to type in measured the same to the
+// pixel. Disabled, the menu keeps the toolkit's disabled arrow, so a menu
+// frozen for a run does not look live.
+func TestAMenuShowsItsArrowInTheAccent(t *testing.T) {
+ test.NewTempApp(t)
+ menu := parts.NewChooser([]string{"avif", "png"}, nil)
+ if got := arrowColourOf(t, menu); got != theme.ColorNamePrimary {
+ t.Errorf("a menu draws its arrow in %q, not in the accent", got)
+ }
+ menu.Disable()
+ if got := arrowColourOf(t, menu); got == theme.ColorNamePrimary {
+ t.Error("a disabled menu still draws its arrow in the accent, so it looks live")
+ }
+}
+
+// arrowColourOf is the colour name the arrow of a menu is drawn in, or
+// nothing when the arrow is not a coloured resource of ours.
+func arrowColourOf(t *testing.T, menu *parts.Chooser) fyne.ThemeColorName {
+ t.Helper()
+ renderer := test.TempWidgetRenderer(t, menu)
+ renderer.Refresh()
+ for _, o := range renderer.Objects() {
+ icon, ok := o.(*widget.Icon)
+ if !ok {
+ continue
+ }
+ if themed, ok := icon.Resource.(*theme.ThemedResource); ok {
+ return themed.ColorName
+ }
+ return ""
+ }
+ t.Fatal("the menu draws no arrow")
+ return ""
+}
+
+// TestAFoldedSectionOpensForARefusalAboutItsField folds File configuration
+// away, refuses the size inside it and asks whether it opened. Every section
+// of a work screen folds since 2026-09-23, and a box marked where nobody can
+// see it reads as a button that did nothing.
+func TestAFoldedSectionOpensForARefusalAboutItsField(t *testing.T) {
+ host := newFakeHost(t)
+ screen := window.NewGenerate(host)
+ content := screen.Object()
+ head := foldTitled(t, content, "", text.SectionConfiguration())
+ head.Tapped(nil)
+ if head.Open() {
+ t.Fatal("pressing the head of File configuration did not fold it, so this guard asks nothing")
+ }
+ fill(t, content, text.FieldSize(), "1")
+ screen.PressPreview()
+ join(host)
+ if !head.Open() {
+ t.Error("the size inside a folded File configuration was refused and the section stayed shut")
+ }
+}
+
+// TestAGroupOfSettingsIsFramedWithARailOfItsKind asks each kind of group for
+// the colour of the rail down its left edge: the accent for a format's
+// settings, the warning colour for a damage's, the colour of a name for notes.
+// The rail is what tells the two groups apart once both are open - the
+// owner's report was that nothing did.
+func TestAGroupOfSettingsIsFramedWithARailOfItsKind(t *testing.T) {
+ for _, kind := range []struct {
+ name string
+ kind parts.GroupKind
+ ink fyne.ThemeColorName
+ }{
+ {"a format's settings", parts.GroupSettings, theme.ColorNamePrimary},
+ {"a damage's settings", parts.GroupDamage, theme.ColorNameWarning},
+ {"notes for the manifest", parts.GroupNotes, parts.ColorNameLabel},
+ } {
+ group := parts.NewInnerFoldingOf(kind.kind, "Settings", parts.Prose("inside")).Object()
+ want := parts.PaletteColour(kind.ink, theme.VariantDark)
+ if !drawsFill(group, want) {
+ t.Errorf("%s: no rail drawn in %s", kind.name, kind.ink)
+ }
+ }
+}
+
+// drawsFill is whether anything under o is a rectangle filled with c.
+func drawsFill(o fyne.CanvasObject, c color.Color) bool {
+ found := false
+ walk(o, func(obj fyne.CanvasObject) {
+ if r, ok := obj.(*canvas.Rectangle); ok && r.FillColor == c {
+ found = true
+ }
+ })
+ return found
+}
+
+// visibleButtonNamed is the button with these words that is shown, where a
+// screen keeps two of one name and only one of them on the screen.
+func visibleButtonNamed(o fyne.CanvasObject, name string) *parts.Button {
+ var found *parts.Button
+ walk(o, func(obj fyne.CanvasObject) {
+ if b, ok := obj.(*parts.Button); ok && b.Text == name && b.Visible() {
+ found = b
+ }
+ })
+ return found
+}
+
+// errorString is a refusal with nothing but words, for a guard marking a box.
+type errorString string
+
+func (e errorString) Error() string { return string(e) }
diff --git a/internal/guard/formwidth_test.go b/internal/guard/formwidth_test.go
index 90132bd..fcd88ef 100644
--- a/internal/guard/formwidth_test.go
+++ b/internal/guard/formwidth_test.go
@@ -100,12 +100,19 @@ func TestABoxForANumberIsTheWidthOfANumber(t *testing.T) {
if screenName == "generate" {
numeric = []string{text.FieldSize(), text.FieldCount(), text.FieldSeed()}
}
+ // One column of the form's grid since 2026-09-23 rather than the
+ // exact width of a number: a field fills the columns it takes, and a
+ // number takes one (parts.Grid) - 185 px at this width, which the
+ // owner looked at in the running window and kept. Two number boxes
+ // side by side is the most a single column may be, so a box wider
+ // than that has taken two columns, which is the stretching this is
+ // about coming back.
for _, label := range numeric {
box := entryUnder(t, content, label)
- if got := box.Size().Width; got > parts.NumericWidth {
- t.Errorf("%s: the box for %q is %.0f px wide, over the %d a number needs.\n"+
- "A box that size promises a value the field does not take.",
- screenName, label, got, parts.NumericWidth)
+ if got := box.Size().Width; got > parts.NumericWidth*2 {
+ t.Errorf("%s: the box for %q is %.0f px wide, over two numbers (%d px), so it has taken more "+
+ "than one column of the form.\nA box that size promises a value the field does not take.",
+ screenName, label, got, parts.NumericWidth*2)
}
}
w.Close()
diff --git a/internal/guard/guitext_test.go b/internal/guard/guitext_test.go
index 2be0293..c6c8491 100644
--- a/internal/guard/guitext_test.go
+++ b/internal/guard/guitext_test.go
@@ -54,8 +54,12 @@ import (
// is not a number, so two sentences a person reads were built outside the text
// package and no carrier list would ever have named them.
var notWords = map[string]string{
- `"10mb"`: "the size a fresh screen starts at, a value rather than prose",
- `"1"`: "how many files a fresh screen starts at",
+ `"10mb"`: "the size a fresh screen starts at, a value rather than prose",
+ `"1"`: "how many files a fresh screen starts at",
+ `"-"`: "the hyphen a size range is written with (1kb-8kb), searched for in a box so a range is not " +
+ "offered a single size in its place - a character of the syntax, never shown",
+ `" "`: "the space two lines of one paragraph of the licence are joined with, so the notice wraps to the " +
+ "window - the words are the notice's own, from version.LicenceNotice",
`"0"`: "the seed a fresh screen starts at",
`"files"`: "the group name a fresh screen starts at, and a recipe value",
`"tfg-gui"`: "recorded in the manifest as the command that ran, a contract value",
@@ -80,6 +84,10 @@ var notWords = map[string]string{
"libraries, asked for by name because that is how the loader takes it",
`"preset"`: "the key the preset field is registered under, not a label",
`"start_from_preset"`: "the key the batch screen's switch to build on a preset is registered under, not a label",
+ `"configuration"`: "the key a folding section is remembered under across a rebuild, never shown - see window/sections.go",
+ `"output"`: "the key a folding section is remembered under across a rebuild, never shown",
+ `"settings"`: "the key a folding section is remembered under across a rebuild, never shown",
+ `"base"`: "the key a folding section is remembered under across a rebuild, never shown",
`"outputDirectory"`: "the name the window files the last output directory under, never shown. " +
"Translating a storage key would lose what was kept the day somebody changed language",
`"windowWidth"`: "the name the window files its width under, never shown",
diff --git a/internal/guard/leftedge_test.go b/internal/guard/leftedge_test.go
index 5ef8798..0f70a00 100644
--- a/internal/guard/leftedge_test.go
+++ b/internal/guard/leftedge_test.go
@@ -64,12 +64,16 @@ func TestEverythingAPersonReadsStartsOnOneLeftEdge(t *testing.T) {
// The title is the word on the tab since 2026-09-15 and the sentence
// under it is what the title used to say - five ranks now, which is
// what this is about.
+ //
+ // The titles of sections and of groups of settings are not asked
+ // since 2026-09-23: every one of them folds, with its arrow IN FRONT
+ // of the words on the owner's word from the running window, so it is
+ // the arrow that stands on this edge and the words follow it. That is
+ // asked by TestTheHeadRowOverhangsTheColumnAndTheTitleDoesNot.
{text.TabOneTarget(), []string{
text.TabOneTarget(),
text.SubtitleGenerate(),
- text.SectionConfiguration(),
text.FieldFormat(),
- text.SettingsFor(firstFormat()),
}},
// The preset screen names its section and its first field with the
// same word, so only one of the two can be found by its text. Asked
@@ -78,7 +82,6 @@ func TestEverythingAPersonReadsStartsOnOneLeftEdge(t *testing.T) {
{text.TabPresets(), []string{
text.TabPresets(),
text.SubtitlePreset(),
- text.SectionPreset(),
text.PresetCatchesHeading(),
}},
{text.TabRecipe(), []string{
diff --git a/internal/guard/screenpixels_test.go b/internal/guard/screenpixels_test.go
index 4f7bf2f..9fa3731 100644
--- a/internal/guard/screenpixels_test.go
+++ b/internal/guard/screenpixels_test.go
@@ -482,7 +482,16 @@ func screenScenes() []screenScene {
// size. Both marks belong to the first batch and both have to appear,
// which is the rule reported on 2026-08-18 - every bad box, not the
// first one.
+ //
+ // EMPTIED rather than assumed empty. The screen opens with its first
+ // batch filled in since #125, so from that day this scene pressed
+ // Preview on a recipe with nothing wrong in it and stored a picture of
+ // a successful preview under the name "refused" - green, and about
+ // nothing. Found by an outside review of #126 and confirmed on the
+ // stored tree: no refusal in it. The runner now refuses to store or
+ // compare a picture named after a refusal that shows none.
{name: "recipe-refused", tab: text.TabRecipe(), set: func(t *testing.T, s scene) {
+ emptyTheFirstBatch(t, s.tab)
pressNamed(t, s.tab, text.ButtonPreview())
}},
// One batch filled in and one not, so the marks are in one block and the
@@ -500,6 +509,9 @@ func screenScenes() []screenScene {
pressNamed(t, s.tab, text.ButtonAddBatch())
fillField(t, s.tab, text.FieldTargetID(), "second")
fillField(t, s.tab, text.FieldSize(), "1kb")
+ // The same repair as the scene above: the first batch arrives
+ // filled in, so it has to be emptied to be the one refused.
+ emptyTheFirstBatch(t, s.tab)
pressNamed(t, s.tab, text.ButtonPreview())
}},
// What an archive holds, which is the one nested repeating thing in this
@@ -577,6 +589,15 @@ func TestEveryScreenStillDrawsItsStoredPicture(t *testing.T) {
for _, sc := range screenScenes() {
t.Run(sc.name, func(t *testing.T) {
got, markup := renderScene(t, sc)
+ // Before anything is compared OR written, so a scene that stopped
+ // reaching its state cannot store a new reference of the wrong
+ // one. See showsARefusal.
+ if strings.Contains(sc.name, "refused") && !showsARefusal(markup) {
+ t.Fatalf("the picture %q is named after a refusal and the screen it drew refuses nothing.\n"+
+ "Reason: a scene that stops reaching its state stays green honestly - this one\n"+
+ "stored a successful preview as \"refused\" for a whole pull request (#125).\n"+
+ "What to do: make the scene put something wrong on the screen before it presses.", sc.name)
+ }
picture := filepath.Join("testdata", "screens", sc.name+".png")
tree := filepath.Join("testdata", "screens", sc.name+".xml")
if writing {
diff --git a/internal/guard/screenrefusal_test.go b/internal/guard/screenrefusal_test.go
new file mode 100644
index 0000000..2784ef2
--- /dev/null
+++ b/internal/guard/screenrefusal_test.go
@@ -0,0 +1,83 @@
+package guard
+
+import (
+ "strings"
+ "testing"
+
+ "fyne.io/fyne/v2"
+
+ "github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
+)
+
+// What the stored pictures of a refusal need in order to be pictures of one.
+// Kept apart from screenpixels_test.go, which runs the scenes, because this is
+// the one question asked of a scene's STATE rather than of its pixels - and
+// the day it was not asked, two pictures named "refused" held a successful
+// preview for a whole pull request (#125, found on #126).
+
+// TestEveryPictureNamedAfterARefusalShowsOne renders the scenes whose name
+// says "refused" and asks each drawn tree for a refusal.
+//
+// The stored pictures ask it too, before they compare or write anything, and
+// that half is what keeps a wrong reference from being stored. This half is
+// the one that can be proven: the stored pictures render every screen, and
+// the mutation runner stops them at its 4 GB ceiling before they finish - so a
+// mutation there comes back CAPPED rather than caught, measured on 2026-09-23.
+// Six scenes fit under the ceiling.
+func TestEveryPictureNamedAfterARefusalShowsOne(t *testing.T) {
+ scenes := screenScenes()
+ // The first render of a process lays out differently - see O148 and the
+ // same call in TestEveryScreenStillDrawsItsStoredPicture.
+ renderScene(t, scenes[0])
+ checked := 0
+ for _, sc := range scenes {
+ if !strings.Contains(sc.name, "refused") {
+ continue
+ }
+ checked++
+ t.Run(sc.name, func(t *testing.T) {
+ if _, markup := renderScene(t, sc); !showsARefusal(markup) {
+ t.Errorf("the picture %q is named after a refusal and the screen it drew refuses nothing.\n"+
+ "What to do: make the scene put something wrong on the screen before it presses.", sc.name)
+ }
+ })
+ }
+ if checked == 0 {
+ t.Fatal("no scene is named after a refusal, so this guard asked nothing")
+ }
+}
+
+// emptyTheFirstBatch clears the two boxes the first batch cannot do without,
+// by position rather than by label - two batches mean two boxes of each name,
+// and the first of them is the one this empties.
+func emptyTheFirstBatch(t *testing.T, o fyne.CanvasObject) {
+ t.Helper()
+ for _, label := range []string{text.FieldTargetID(), text.FieldSize()} {
+ boxes := entriesUnder(o, label)
+ if len(boxes) == 0 {
+ t.Fatalf("there is no %q box on this screen to empty", label)
+ }
+ boxes[0].SetText("")
+ }
+}
+
+// showsARefusal says whether a stored tree holds words in the error colour -
+// a refusal under a field or at the foot of the form. The star of a required
+// field is in the same colour and is on every screen at rest, so a text that
+// is only the star does not count.
+func showsARefusal(markup string) bool {
+ for _, line := range strings.Split(markup, "\n") {
+ if !strings.Contains(line, `color="error"`) || !strings.Contains(line, "")
+ end := strings.LastIndex(line, "")
+ if start < 0 || end <= start {
+ continue
+ }
+ if words := strings.TrimSpace(line[start+1 : end]); words != "" && words != "*" {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/guard/screentext_test.go b/internal/guard/screentext_test.go
index a695328..6bbd344 100644
--- a/internal/guard/screentext_test.go
+++ b/internal/guard/screentext_test.go
@@ -57,7 +57,9 @@ func TestTheBatchScreenSaysWhatAnEmptyBoxWillDo(t *testing.T) {
t.Errorf("the count box arrives holding %q. A box with a value in it cannot say the setting "+
"was left unstated, which is what keeps the key out of the recipe.", box.Text)
}
- if want := strconv.Itoa(recipe.DefaultCount); box.PlaceHolder != want {
+ // Worded as a default since 2026-09-23 ("default: 1"): a bare grey "1"
+ // was read in the running window as a value somebody had typed.
+ if want := text.PlaceholderLeftEmpty(strconv.Itoa(recipe.DefaultCount)); box.PlaceHolder != want {
t.Errorf("the count box shows %q as what happens if it is left alone and the recipe uses %q",
box.PlaceHolder, want)
}
diff --git a/internal/guard/spacingscale_test.go b/internal/guard/spacingscale_test.go
index 1cdae5d..0eb52f1 100644
--- a/internal/guard/spacingscale_test.go
+++ b/internal/guard/spacingscale_test.go
@@ -32,11 +32,11 @@ import (
// declares them proves they were declared, which is not the question.
//
// Over means the name ends above the box begins. One edge means the name and
-// its box start on one X, and every box on the screen starts on that X - a
-// stacked form has no column of names to line up, so the left edge is the
-// only line there is, and a box indented under its name reads as a child of
-// it rather than as the value it holds. Apart means two fields have room
-// between them, or the form is a list with no rhythm.
+// its box start on one X - a box indented under its name reads as a child of
+// it rather than as the value it holds - and every box starts on the edge of
+// one of the grid's columns (since 2026-09-23, parts.Grid - before that there
+// was one column and one edge). Apart means two fields in one column have
+// room between them, or the form is a list with no rhythm.
func TestANameStandsOverItsBoxOnOneEdgeWithEveryOther(t *testing.T) {
ourTheme(t)
content, _ := laidOutWindow(t)
@@ -61,27 +61,61 @@ func TestANameStandsOverItsBoxOnOneEdgeWithEveryOther(t *testing.T) {
edges[box.X] = append(edges[box.X], label)
boxes = append(boxes, box)
}
- if len(edges) != 1 {
- t.Errorf("the boxes on the generate screen start on %d different edges, and a form lines up only while they start on one: %v",
- len(edges), edges)
+ // Columns since 2026-09-23 (parts.Grid): the boxes start on the edges of
+ // the grid's columns rather than on one, and the first of those is the
+ // edge the first field stands on. More edges than the grid has columns
+ // is a box standing between two of them.
+ if len(edges) > parts.GridColumns {
+ t.Errorf("the boxes on the generate screen start on %d different edges, more than the %d columns the form "+
+ "is laid in, so some box stands off every column: %v", len(edges), parts.GridColumns, edges)
}
- for i := 1; i < len(boxes); i++ {
- if boxes[i].Y <= boxes[i-1].Y+boxes[i-1].Height {
+ if first := boxes[0].X; len(edges[first]) == 0 || boxes[0].X > minEdge(edges)+1 {
+ t.Errorf("the first box starts at x=%.1f and the leftmost at x=%.1f - the form's first column is where it begins",
+ first, minEdge(edges))
+ }
+ // Apart, and the pairing, asked down each column: a field stands under
+ // the one above it in its own column, while the one beside it shares its
+ // row. The pairing - a name is nearer the box under it than that box is
+ // to the next name - is asked for each pair down the screen rather than
+ // once at the top, after the outside review of #116: measured once, a
+ // later pair could drift while the first still held.
+ for i := 0; i < len(names); i++ {
+ j := nextInColumn(boxes, i)
+ if j < 0 {
+ continue
+ }
+ if boxes[j].Y <= boxes[i].Y+boxes[i].Height {
t.Errorf("%q and %q are not apart: one box ends at %.1f and the next begins at %.1f",
- names[i-1], names[i], boxes[i-1].Y+boxes[i-1].Height, boxes[i].Y)
+ names[i], names[j], boxes[i].Y+boxes[i].Height, boxes[j].Y)
}
- }
- // The pairing: a name is nearer the box under it than that box is to the
- // next name. Asked for each pair down the screen rather than once at the
- // top, after the outside review of #116: measured once, a later pair
- // could drift while the first still held.
- for i := 0; i+1 < len(names); i++ {
- between := gapBelowField(t, generate, names[i], names[i+1])
+ between := gapBelowField(t, generate, names[i], names[j])
if nameGaps[i] >= between {
t.Errorf("%q stands %.1f px over its box and %q stands %.1f px under that box, so nothing says which box the name belongs to",
- names[i], nameGaps[i], names[i+1], between)
+ names[i], nameGaps[i], names[j], between)
+ }
+ }
+}
+
+// nextInColumn is the first field after i that starts on i's column, or -1.
+func nextInColumn(boxes []band, i int) int {
+ for j := i + 1; j < len(boxes); j++ {
+ if off := boxes[j].X - boxes[i].X; off <= 1 && off >= -1 {
+ return j
+ }
+ }
+ return -1
+}
+
+// minEdge is the leftmost of the edges boxes start on.
+func minEdge(edges map[float32][]string) float32 {
+ first := true
+ var least float32
+ for x := range edges {
+ if first || x < least {
+ least, first = x, false
}
}
+ return least
}
// Two sections are the same distance apart wherever they are.
diff --git a/internal/guard/testdata/screens/about.png b/internal/guard/testdata/screens/about.png
index a2ea237..f9e4bfd 100644
Binary files a/internal/guard/testdata/screens/about.png and b/internal/guard/testdata/screens/about.png differ
diff --git a/internal/guard/testdata/screens/about.xml b/internal/guard/testdata/screens/about.xml
index 4fbd1bc..60dfad9 100644
--- a/internal/guard/testdata/screens/about.xml
+++ b/internal/guard/testdata/screens/about.xml
@@ -29,106 +29,105 @@
-
-
-
-
-
-
-
-
-
- Testing Files Generator 0.3.0
-
+
+
+
+
+
+
+
+
+ Testing Files Generator 0.3.0
-
-
-
-
-
-
-
-
- Generate test files, and know how the system under test should react to them.
-
+
+
+
+
+
+
+
+
+
+ Generate test files, and know how the system under test should react to them.
-
-
-
-
-
-
-
- How to use it
-
-
-
-
-
-
- Choose a preset, or fill in one batch on the first screen.
+
+
+
+
+
+
+
+
+ How to use it
+
+
+
+ •
+
+
+
+
+ Choose a preset, or fill in one batch on the first screen.
- •
-
-
-
-
-
- Press Generate. The files and a manifest land in the output folder.
+
+ •
+
+
+
+
+ Press Generate. The files and a manifest land in the output folder.
- •
-
-
-
-
-
- Point your test at the manifest. For every file it says what the system under test should do with it - accept it, reject it or sanitize it - or
- records the outcome as unspecified, where the right answer belongs to the application's own policy.
+
+ •
+
+
+
+
+ Point your test at the manifest. For every file it says what the system under test should do with it - accept it, reject it
+ or sanitize it - or records the outcome as unspecified, where the right answer belongs to the application's own
+ policy.
- •
-
-
-
-
- Licence
-
-
-
-
+
+
+
+
+
+ Licence
+
+
+
+
+
Testing Files Generator
Copyright (C) 2026 DonislawDev
Released under the GNU General Public License, version 3. The full text is at
- https://www.gnu.org/licenses/gpl-3.0.html, and in the LICENSE file if you have
- the source or the full release.
-
- There is no warranty, to the extent the law allows.
-
- The files you generate are yours. Generated files, recipes and manifests are
- output of this program and not derived works of it, so this licence does not
- reach them. You can generate fixtures, commit them and ship them inside a
- closed source product with no obligation of any kind.
-
- Code from other projects is compiled into this program. Their licences and
- copyright notices are in THIRD-PARTY-NOTICES.md, which comes with the source
- and with the full release.
-
+ https://www.gnu.org/licenses/gpl-3.0.html, and in the LICENSE file if you have the source or the full release.
+
+ There is no warranty, to the extent the law allows.
+
+ The files you generate are yours. Generated files, recipes and manifests are output of this program and not derived
+ works of it, so this licence does not reach them. You can generate fixtures, commit them and ship them inside a closed
+ source product with no obligation of any kind.
+
+ Code from other projects is compiled into this program. Their licences and copyright notices are in
+ THIRD-PARTY-NOTICES.md, which comes with the source and with the full release.
@@ -136,12 +135,14 @@
-
-
-
-
- Support
-
+
+
+
+
+
+ Support
+
+
@@ -150,7 +151,14 @@
-
+
+
+
+
+ Donate
+
+
+
@@ -162,131 +170,1219 @@
-
-
-
-
- Third party code compiled in
-
-
-
-
- Go runtime and standard library BSD-3-Clause Copyright 2009 The Go Authors
- fyne.io/fyne/v2 BSD-3-Clause (C) 2018 Fyne.io developers (see AUTHORS)
- fyne.io/systray Apache-2.0 2014 Brave New Software Project, Inc.
- github.com/BurntSushi/toml MIT (c) 2013 TOML authors
- github.com/FyshOS/fancyfs BSD-3-Clause (C) 2025 FyshOS developers (see AUTHORS)
- github.com/anthonynsimon/bild MIT (c) 2021 Anthony Najjar Simon
- github.com/clipperhouse/uax29/v2 MIT (c) 2020 Matt Sherman
- github.com/fsnotify/fsnotify BSD-3-Clause Copyright 2012 The Go Authors
- github.com/fyne-io/image BSD-3-Clause (c) 2022, Fyne.io
- github.com/fyne-io/oksvg BSD-3-Clause (c) 2018, Steven R Wiley
- github.com/gen2brain/gav1d BSD-2-Clause (c) 2018-2025 VideoLAN and dav1d authors, (c) 2016 Alliance for Open
- Media
- github.com/gen2brain/jxl BSD-3-Clause (c) the JPEG XL Project Authors
- github.com/go-gl/gl MIT (c) 2014 Eric Woroshow
- github.com/go-gl/glfw/v3.4/glfw BSD-3-Clause (c) 2012 The glfw3-go Authors. All rights reserved.
- github.com/go-text/render BSD-3-Clause 2021 The go-text authors
- github.com/go-text/typesetting BSD-3-Clause 2021 The go-text authors
- github.com/goccy/go-yaml MIT (c) 2019 Masaaki Goshima
- github.com/godbus/dbus/v5 BSD-2-Clause (c) 2013, Georg Reinke (), Google
- github.com/jeandeaual/go-locale MIT (c) 2020 Alexis Jeandeau
- github.com/jsummers/gobmp MIT (c) 2012-2015 Jason Summers
- github.com/mattn/go-runewidth MIT (c) 2016 Yasuhiro Matsumoto
- github.com/nfnt/resize ISC (c) 2012, Jan Schlicht
- github.com/nicksnyder/go-i18n/v2 MIT (c) 2014 Nick Snyder https://github.com/nicksnyder
- github.com/rymdport/portal Apache-2.0
- github.com/srwiley/oksvg BSD-3-Clause (c) 2018, Steven R Wiley
- github.com/srwiley/rasterx BSD-3-Clause (c) 2018, Steven R Wiley
- github.com/yuin/goldmark MIT (c) 2019 Yusuke Inuzuka
- golang.org/x/image BSD-3-Clause 2009 The Go Authors.
- golang.org/x/net BSD-3-Clause 2009 The Go Authors.
- golang.org/x/sys BSD-3-Clause 2009 The Go Authors.
- golang.org/x/text BSD-3-Clause Copyright 2009 The Go Authors
-
+
+
+
+
+
+ Third party code compiled in
+
+
+
+
+
+
+
+ Go runtime and standard library
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ Copyright 2009 The Go Authors
+
+
+
-
-
-
-
-
-
-
-
- Files compiled in that are not code
-
-
-
-
- DejaVu Sans Mono for Powerline Bitstream-Vera Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. DejaVu
- changes are in public domain
- EmojiOne Color MIT Copyright 2016 Adobe Systems Incorporated
- Fyne icon set BSD-3-Clause (C) 2018 Fyne.io developers (see AUTHORS)
- Fyne shaders BSD-3-Clause (C) 2018 Fyne.io developers (see AUTHORS)
- Fyne translations BSD-3-Clause (C) 2018 Fyne.io developers (see AUTHORS)
- Inter OFL-1.1 Copyright 2016 The Inter Project Authors
- Inter Symbols OFL-1.1 (c) 2020 The Inter Project Authors
- Noto Sans OFL-1.1 Copyright 2015 Google Inc. All Rights Reserved.
-
+
+
+
+
+
+ fyne.io/fyne/v2
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ (C) 2018 Fyne.io developers (see AUTHORS)
+
+
+
-
-
-
-
-
-
-
-
- Shipped beside the program on Windows
-
-
-
-
- Mesa 3D, llvmpipe software renderer 26.2.0 MIT AND Apache-2.0 WITH LLVM-exception AND BSL-1.0 Copyright (C)
- 1999-2007 Brian Paul, Copyright (C) 2008 VMware, Inc.
-
+
+
+
+
+
+ fyne.io/systray
+
+
+
+
+
+
+
+
+ Apache-2.0
+
+
+
+
+
+
+
+
+ 2014 Brave New Software Project, Inc.
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Donate
-
-
-
-
-
-
-
+
+
+
+
+
+ github.com/BurntSushi/toml
+
+
+
+
+
+
+
+
+ MIT
+
+
+
+
+
+
+
+
+ (c) 2013 TOML authors
+
+
+
+
+
+
+
+
+
+
+ github.com/FyshOS/fancyfs
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ (C) 2025 FyshOS developers (see AUTHORS)
+
+
+
+
+
+
+
+
+
+
+ github.com/anthonynsimon/bild
+
+
+
+
+
+
+
+
+ MIT
+
+
+
+
+
+
+
+
+ (c) 2021 Anthony Najjar Simon
+
+
+
+
+
+
+
+
+
+
+ github.com/clipperhouse/uax29/v2
+
+
+
+
+
+
+
+
+ MIT
+
+
+
+
+
+
+
+
+ (c) 2020 Matt Sherman
+
+
+
+
+
+
+
+
+
+
+ github.com/fsnotify/fsnotify
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ Copyright 2012 The Go Authors
+
+
+
+
+
+
+
+
+
+
+ github.com/fyne-io/image
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ (c) 2022, Fyne.io
+
+
+
+
+
+
+
+
+
+
+ github.com/fyne-io/oksvg
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ (c) 2018, Steven R Wiley
+
+
+
+
+
+
+
+
+
+
+ github.com/gen2brain/gav1d
+
+
+
+
+
+
+
+
+ BSD-2-Clause
+
+
+
+
+
+
+
+
+ (c) 2018-2025 VideoLAN and dav1d authors, (c) 2016 Alliance
+ for Open Media
+
+
+
+
+
+
+
+
+
+
+ github.com/gen2brain/jxl
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ (c) the JPEG XL Project Authors
+
+
+
+
+
+
+
+
+
+
+ github.com/go-gl/gl
+
+
+
+
+
+
+
+
+ MIT
+
+
+
+
+
+
+
+
+ (c) 2014 Eric Woroshow
+
+
+
+
+
+
+
+
+
+
+ github.com/go-gl/glfw/v3.4/glfw
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ (c) 2012 The glfw3-go Authors. All rights reserved.
+
+
+
+
+
+
+
+
+
+
+ github.com/go-text/render
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ 2021 The go-text authors
+
+
+
+
+
+
+
+
+
+
+ github.com/go-text/typesetting
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ 2021 The go-text authors
+
+
+
+
+
+
+
+
+
+
+ github.com/goccy/go-yaml
+
+
+
+
+
+
+
+
+ MIT
+
+
+
+
+
+
+
+
+ (c) 2019 Masaaki Goshima
+
+
+
+
+
+
+
+
+
+
+ github.com/godbus/dbus/v5
+
+
+
+
+
+
+
+
+ BSD-2-Clause
+
+
+
+
+
+
+
+
+ (c) 2013, Georg Reinke (), Google
+
+
+
+
+
+
+
+
+
+
+ github.com/jeandeaual/go-locale
+
+
+
+
+
+
+
+
+ MIT
+
+
+
+
+
+
+
+
+ (c) 2020 Alexis Jeandeau
+
+
+
+
+
+
+
+
+
+
+ github.com/jsummers/gobmp
+
+
+
+
+
+
+
+
+ MIT
+
+
+
+
+
+
+
+
+ (c) 2012-2015 Jason Summers
+
+
+
+
+
+
+
+
+
+
+ github.com/mattn/go-runewidth
+
+
+
+
+
+
+
+
+ MIT
+
+
+
+
+
+
+
+
+ (c) 2016 Yasuhiro Matsumoto
+
+
+
+
+
+
+
+
+
+
+ github.com/nfnt/resize
+
+
+
+
+
+
+
+
+ ISC
+
+
+
+
+
+
+
+
+ (c) 2012, Jan Schlicht
+
+
+
+
+
+
+
+
+
+
+ github.com/nicksnyder/go-i18n/v2
+
+
+
+
+
+
+
+
+ MIT
+
+
+
+
+
+
+
+
+ (c) 2014 Nick Snyder https://github.com/nicksnyder
+
+
+
+
+
+
+
+
+
+
+ github.com/rymdport/portal
+
+
+
+
+
+
+
+
+ Apache-2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ github.com/srwiley/oksvg
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ (c) 2018, Steven R Wiley
+
+
+
+
+
+
+
+
+
+
+ github.com/srwiley/rasterx
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ (c) 2018, Steven R Wiley
+
+
+
+
+
+
+
+
+
+
+ github.com/yuin/goldmark
+
+
+
+
+
+
+
+
+ MIT
+
+
+
+
+
+
+
+
+ (c) 2019 Yusuke Inuzuka
+
+
+
+
+
+
+
+
+
+
+ golang.org/x/image
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ 2009 The Go Authors.
+
+
+
+
+
+
+
+
+
+
+ golang.org/x/net
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ 2009 The Go Authors.
+
+
+
+
+
+
+
+
+
+
+ golang.org/x/sys
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ 2009 The Go Authors.
+
+
+
+
+
+
+
+
+
+
+ golang.org/x/text
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ Copyright 2009 The Go Authors
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Files compiled in that are not code
+
+
+
+
+
+
+
+ DejaVu Sans Mono for Powerline
+
+
+
+
+
+
+
+
+ Bitstream-Vera
+
+
+
+
+
+
+
+
+ Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.
+ DejaVu changes are in public domain
+
+
+
+
+
+
+
+
+
+
+ EmojiOne Color
+
+
+
+
+
+
+
+
+ MIT
+
+
+
+
+
+
+
+
+ Copyright 2016 Adobe Systems Incorporated
+
+
+
+
+
+
+
+
+
+
+ Fyne icon set
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ (C) 2018 Fyne.io developers (see AUTHORS)
+
+
+
+
+
+
+
+
+
+
+ Fyne shaders
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ (C) 2018 Fyne.io developers (see AUTHORS)
+
+
+
+
+
+
+
+
+
+
+ Fyne translations
+
+
+
+
+
+
+
+
+ BSD-3-Clause
+
+
+
+
+
+
+
+
+ (C) 2018 Fyne.io developers (see AUTHORS)
+
+
+
+
+
+
+
+
+
+
+ Inter
+
+
+
+
+
+
+
+
+ OFL-1.1
+
+
+
+
+
+
+
+
+ Copyright 2016 The Inter Project Authors
+
+
+
+
+
+
+
+
+
+
+ Inter Symbols
+
+
+
+
+
+
+
+
+ OFL-1.1
+
+
+
+
+
+
+
+
+ (c) 2020 The Inter Project Authors
+
+
+
+
+
+
+
+
+
+
+ Noto Sans
+
+
+
+
+
+
+
+
+ OFL-1.1
+
+
+
+
+
+
+
+
+ Copyright 2015 Google Inc. All Rights Reserved.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Shipped beside the program on Windows
+
+
+
+
+
+
+
+ Mesa 3D, llvmpipe software
+ renderer 26.2.0
+
+
+
+
+
+
+
+
+ MIT AND Apache-2.0 WITH
+ LLVM-exception AND BSL-1.0
+
+
+
+
+
+
+
+
+ Copyright (C) 1999-2007 Brian Paul,
+ Copyright (C) 2008 VMware, Inc.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/internal/guard/testdata/screens/catalogue.png b/internal/guard/testdata/screens/catalogue.png
index 55c2c7b..a3e2b95 100644
Binary files a/internal/guard/testdata/screens/catalogue.png and b/internal/guard/testdata/screens/catalogue.png differ
diff --git a/internal/guard/testdata/screens/catalogue.xml b/internal/guard/testdata/screens/catalogue.xml
index 159d2a1..17918d6 100644
--- a/internal/guard/testdata/screens/catalogue.xml
+++ b/internal/guard/testdata/screens/catalogue.xml
@@ -1,7 +1,7 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Write a label inside each generated file, including the ones that are far too small to hold it
+
+
+
+
+
+
+ every value with the kind of thing it is
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
-
-
-
-
-
- png
+
+
+
+
+ bmp
+
-
-
-
-
-
-
-
-
+
+
+
+
+ csv
+
+
+
+
+
+
+
+ docx
+
+
+
+
+
+
+ gif
+
+
+
+
+
+
+ html
+
+
+
+
+
+
+ ico
+
+
+
+
+
+
+ jpg
+
+
+
+
+
+
+ json
+
+
+
+
+
+
+ jxl
+
+
+
+
+
+
+ log
+
+
+
+
+
+
+ md
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ a long value
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too small to hold it
+
+
+
+
+
+
+ png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Tabs
-
-
-
-
-
- the strip, first word chosen
+
+
+
+
+
+
+ the strip, first word chosen
+
+
+
+
+
+
+
+ Single batch
-
-
-
-
-
-
- Single batch
-
-
-
-
- Presets
-
-
-
-
- Several batches
-
-
-
-
- About
-
-
-
-
-
-
-
-
-
- a word under the pointer
-
+
+
+
+ Presets
-
-
-
-
-
-
- Single batch
-
-
-
-
- Presets
-
-
-
-
- Several batches
-
-
-
-
- About
-
-
-
-
-
-
-
-
-
- a word holding the keyboard
-
+
+
+
+ Several batches
-
-
-
-
-
-
- Single batch
-
-
-
-
- Presets
-
-
-
-
- Several batches
-
-
-
-
- About
-
-
-
-
-
-
-
-
-
- the strip with its screens under it
-
+
+
+
+ About
-
-
-
-
-
-
-
-
-
-
- Single batch
-
-
-
-
- Presets
-
-
-
-
- Several batches
-
-
-
-
- About
+
+
+
+
+
+
+
+
+ a word under the pointer
-
-
-
-
-
-
- the first screen
+
+
+
+
+
+ Single batch
+
+
+
+
+ Presets
+
+
+
+
+ Several batches
+
+
+
+
+ About
+
+
+
+
+
+
+
+
+
+ a word holding the keyboard
-
-
-
-
-
-
-
- long words
-
+
+
+
+
+ Single batch
-
-
-
-
-
-
- Write a label inside each generated file, including the ones that are far too small to hold it
-
-
-
-
- Short
-
-
-
-
-
-
-
-
-
-
-
- Fields
-
-
-
-
-
- a row
-
+
+
+
+ Presets
-
-
-
-
- Size
-
-
-
-
-
-
-
-
- 10mb
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- a row that has to be filled in
-
+
+
+
+ Several batches
-
-
-
-
-
- Size
-
- *
+
+
+
+ About
+
+
+
+
+
+
+
+
+
+ the strip with its screens under it
+
-
-
-
-
-
-
-
-
- 10mb
-
-
+
+
+
+
+
+
+
+
+
+ Single batch
-
-
-
-
-
-
-
-
-
-
-
-
- a row the run refused
-
-
-
-
-
-
- Size
-
-
-
-
-
-
-
-
- 10mb
-
-
+
+
+
+ Presets
+
+
+
+
+ Several batches
+
+
+
+ About
+
+
-
-
-
-
+
- a size of 3 B is below the smallest png, which is 73 B
+ the first screen
-
-
-
-
-
-
- a row frozen while a run goes
+
+
+
+
+
+ long words
+
+
+
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too small to hold it
-
-
-
-
- Size
-
-
-
-
-
-
-
-
- 10mb
+
+
+
+ Short
+
+
+
+
+
+
+
+
+
+
+
+
+ Fields
+
+
+
+
+
+
+ a row
+
+
+
+
+
+
+
+ Size
+
+
+
+
+
+
+
+
+
+ 10mb
+
-
-
+
+
-
-
-
-
-
-
- a switch under its name
-
-
-
-
-
-
- Label in each file
-
-
-
-
-
-
-
-
-
-
-
-
-
- a named control with no setting behind it
-
-
-
-
-
- How the size is given
-
-
-
-
-
-
- One size
- A range
- Around a limit
-
-
-
-
-
-
-
-
-
- a table: the names once over the columns, cells under them
-
-
-
-
-
-
- Name
-
- Size
-
- *
+
+
+
+
+
+ a row that has to be filled in
+
-
-
-
-
-
-
-
-
-
-
-
- report.txt
-
-
-
+
+
+
+
+ Size
+
+ *
-
-
-
-
-
-
-
-
-
- 10kb
+
+
+
+
+
+
+
+
+ 10mb
-
-
-
-
-
-
-
-
-
- Remove
-
+
-
-
-
-
-
-
-
-
-
-
- invoice.pdf
-
-
-
+
+
+
+
+
+
+ a row the run refused
-
+
+
+
+
+
+
+ Size
-
-
-
-
-
-
-
-
- 2mb
+
+
+
+
+
+
+
+
+ 10mb
-
+
-
-
-
-
-
-
- Remove
+
+
+
+
+
+
+ a size of 3 B is below the smallest png, which is 73 B
+
-
-
+
-
-
-
-
-
-
- a row of arbitrary cells
-
-
-
-
-
-
-
-
-
-
- Anything
-
+
+
+
+
+
+ a row frozen while a run goes
+
+
+
+
+
+
+ Size
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
-
-
-
-
-
-
- Choose...
-
+
+
+
+
-
-
-
-
-
-
- a long name
-
-
-
-
-
-
- Write a label inside each generated file, including the ones that are far too small to hold it
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ a box to tick with its name beside it
-
+
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
-
-
-
-
-
-
-
-
- PropertyField
-
-
-
-
-
- a number with a range
+
+
+
+
+
+ a box to tick beside a named field, level with its box
+
-
-
-
-
-
-
-
-
- Width
-
-
-
-
-
+
+
+
+
+
+
+ Seed
-
-
-
-
-
-
-
-
- 800
+
+
+
+
+
+
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
- a choice from a closed set
-
-
-
-
-
-
-
-
-
- Compression
-
-
+
+
+
+
-
+
+
-
-
-
-
-
-
-
- deflate
-
-
-
-
+
+ Label in each file
+
+
-
-
-
-
-
-
-
-
-
- a yes or no
-
-
-
-
-
-
-
-
-
- Bom
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ a named control with no setting behind it
+
+
+
+
+
+
+ How the size is given
+
+
+
+
+
+
+ One size
+ A range
+ Around a limit
+
+
-
-
-
-
-
-
-
-
- a size
+
+
+
+
+
+ a table: the names once over the columns, cells under them
+
-
-
-
-
-
-
-
-
- Member size
-
-
-
-
+
+
+
+
+
+ Name
+
+
+ Size
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+ report.txt
+
+
+
+
-
-
-
-
-
-
-
-
- 1kb
-
-
-
+
+
+
+
+
+
+
+
+ 10kb
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+ Remove
-
+
+
-
-
-
-
-
-
-
-
-
-
- free text
-
-
-
-
-
-
-
-
-
- Password
-
-
-
-
+
+
+
+
+
+
+
+
+
+ invoice.pdf
+
+
+
+
-
-
-
-
-
-
-
-
- worked out from the size
-
-
-
+
+
+
+
+
+
+
+
+ 2mb
-
+
+
+
+
+
+
+
+ Remove
+
+
+
+
-
-
-
-
-
-
-
-
-
-
- ByteCount
-
-
-
-
-
- a size counted out
+
+
+
+
+
+ a row of arbitrary cells
+
-
-
-
-
-
- Size
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+
+
+
+
+ Anything
-
-
-
-
-
-
-
-
- 10 485 760 B
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+ Choose...
+
+
-
-
-
-
-
-
- nothing to count yet
+
+
+
+
+
+ a long name
+
-
-
-
-
-
- Size
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too small to hold it
+
+
+
+
+
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- a large size
+
+
+
+
+
+
+
+ PropertyField
+
+
+
+
+
+
+ a number with a range
+
-
-
-
-
-
- Size
-
-
-
-
-
-
-
-
- 2gb
-
+
+
+
+
+
+
+
+ Width
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+ default: 800
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
- 2 147 483 648 B
-
-
-
-
+
+
-
-
-
-
-
-
-
-
- Tips
-
-
-
-
-
- the button beside a name
-
-
-
-
-
-
-
-
- Size
-
-
-
-
+
+
+
+
+
+ a choice from a closed set
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Compression
+
+
+
+
+
+
+
+
+
+
+
+
+ deflate
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
-
-
-
- the button under the pointer
+
+
+
+
+
+ a yes or no
+
-
-
-
-
-
-
+
+
+
+
- Size
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Bom
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ a size
+
+
+
+
+
+
+
+
+
+ Member size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: 1kb
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
-
+
-
+
+
-
-
-
-
-
-
-
-
-
-
-
- 10mb The exact size every file will have, to the byte.
- Units count in 1024s, so 10mb is 10 485 760 B.
-
+
+
+
+
+
+
+ free text
+
+
+
+
+
+
+
+
+
+ Password
+
+
+
+
-
+
+
+
+
+
+
+
+
+ worked out from the size
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
- ErrorArea
-
-
-
-
-
- silent
+
+
+
+
+ ByteCount
+
+
+
+
+
+
+ a size counted out
+
-
-
-
-
-
-
-
-
-
- one line
+
+
+
+
+
+ Size
+
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
+
-
-
-
-
-
-
- size 3 B is below the smallest png, which is 73 B
+
+
+
+
+
+
+ nothing to count yet
-
-
-
-
-
-
-
- a long message
+
+
+
+ Size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
- Write a label inside each generated file, including the ones that are far too small to hold it. Write a label inside each
- generated file, including the ones that are far too small to hold it
+
+
+
+
+
+
+ a large size
-
-
-
-
-
-
-
-
-
- Progress
-
-
-
-
-
- nothing done yet
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- half done
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- done
+
+
+
+ Size
+
+
+
+
+
+
+
+
+
+ 2gb
+
+
+
+
+
+
+
+
+
+
+
+ 2 147 483 648 B
+
+
+
-
+
-
-
-
-
-
+
-
-
-
-
- Folding
-
-
-
-
-
- open
+
+
+
+
+ Tips
+
+
+
+
+
+
+ the button beside a name
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ Size
+
+
+
+
+
-
-
-
-
-
-
- inside the fold
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
- closed
+
+
+
+
+
+ the button under the pointer
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
-
-
-
-
+
+
+
+
+
+
+ Size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- closed, saying what it holds
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for png
-
-
-
-
-
-
-
- width 800, height 600
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ 10mb The exact size every file will have, to the byte.
+ Units count in 1024s, so 10mb is 10 485 760 B.
-
+
-
-
-
+
-
-
-
-
-
- inner, open
+
+
+
+
+
+
+
+ ErrorArea
+
+
+
+
+
+
+ silent
+
-
-
-
-
-
-
-
-
-
-
-
-
-
- Advanced
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ one line
+
+
+
+
+
- inside the inner fold
+ size 3 B is below the smallest png, which is 73 B
-
-
-
-
-
-
- inner, closed
+
+
+
+
+
+ a long message
+
+
+
+
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too small to hold it. Write a label inside each
+ generated file, including the ones that are far too small to hold it
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- Advanced
-
-
-
-
-
+
+
+
+
+
+
+ offering the fix it asks for
+
+
+
+
+
+
+
+
+
+ AVIF cannot be smaller than 311 B. Requested: 1 B
+
+
+
+
+
+
+
+
+ Use the smallest size, 311 B
+
+
+
+
+
+
+
+
+
+
+
+
+ Progress
+
+
+
+
+
+
+ nothing done yet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ half done
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ done
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Folding
+
+
+
+
+
+
+ open
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
-
+
+
+
-
-
+
+
+
+
+
+ inside the fold
+
+
+
+
+
-
-
-
-
-
-
- a long title
+
+
+
+
+
+ closed
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
- Write a label inside each generated file, including the ones that are far too small to hold it
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
-
-
+
+
-
-
-
-
-
- inside the fold
+
+
+
+
+
+
+
+
+ closed, saying what it holds
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+ Settings for png
+
+
+
+
+ width 800, height 600
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
- the head under the pointer
+
+
+
+
+
+ inner, open
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+ Advanced
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
- inside the fold
-
+
+
+
+
+
+ inside the inner fold
+
+
+
-
+
-
-
-
-
-
-
- the head holding the keyboard
+
+
+
+
+
+ inner, closed
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+ Advanced
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
- inside the fold
-
+
+
+
+
+
+
+
+
+ inner, a damage's settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for zero-head
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ inside the inner fold
+
+
+
-
+
-
-
-
-
-
-
- shut, the head under the pointer
+
+
+
+
+
+ inner, notes for the manifest
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for png
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
- width 800, height 600
-
+
+
+
+
+
+ Notes for the manifest
+
+
+
-
+
+
+
-
+
+
+
+
+
+ inside the inner fold
+
+
+
+
-
-
-
-
-
-
-
-
- Title
-
-
-
-
-
- the title of a screen
-
-
-
-
-
-
-
-
- Single batch
-
-
-
-
-
-
-
-
-
-
- the sentence under a title
-
-
-
-
-
-
-
-
- Files of one format and one size, with a manifest that says how the system under test should react to them.
-
-
-
-
-
-
-
-
-
-
- a title with its sentence
-
-
-
-
-
-
-
-
-
- Single batch
-
-
-
-
-
-
-
-
- Files of one format and one size, with a manifest that says how the system under test should react to them.
+
+
+
+
+
+ a long title
-
-
-
-
-
-
-
- the name of a field
-
-
-
-
- Output directory
-
-
-
-
-
-
- the name of a block inside a section
-
-
-
-
-
-
-
-
- Typically finds
-
-
-
-
-
-
-
-
-
-
- a paragraph
-
-
-
-
-
-
-
-
- Every file has exactly the size you ask for, to the byte, or the run refuses before writing anything.
-
-
-
-
-
-
-
-
-
-
- a note that has to stay visible
-
-
-
-
-
-
-
-
- The limit of 100 000 files is this tool's, not the disk's.
-
-
-
-
-
-
-
-
-
-
- a list of short statements
-
-
-
-
-
-
-
-
-
-
- Upload validators
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too small to hold it
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ inside the fold
+
+
+
-
+
-
- •
+
-
-
-
-
-
- Size limits
-
+
+
+
+
+
+
+ the head under the pointer
-
-
- •
-
-
-
-
-
-
- Archive handling
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ inside the fold
+
+
+
-
+
-
- •
+
-
-
-
-
-
-
- a list whose items do not fit on one line
+
+
+
+
+
+ the head holding the keyboard
+
-
-
-
-
-
-
-
-
-
- One line
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ inside the fold
+
+
+
-
+
-
- •
+
-
-
-
-
-
- Write a label inside each generated file, including the ones that are far too small to hold it Write a label inside each generated file,
- including the ones that are far too small to hold it
-
+
+
+
+
+
+
+ shut, the head under the pointer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for png
+
+
+
+
+ width 800, height 600
+
+
+
+
+
+
+
+
+
+
+
-
- •
+
-
-
-
-
-
- a caption, the smallest rank
-
-
-
-
-
-
-
-
- 10 485 760 B
-
-
-
-
-
-
-
-
-
-
- a long line at every rank
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+
+ the title of a screen
+
-
-
-
-
-
+
+
+
- Write a label inside each generated file, including the ones that are far too sma…
+ Single batch
- Write a label inside each generated file, including the ones that are far too small to hold it
-
-
-
-
- Write a label inside each generated file, including the ones that are far too small to hold it
+
+
+
+
+
+
+ the sentence under a title
-
+
- Write a label inside each generated file, including the ones that are far too small to hold it
+ Files of one format and one size, with a manifest that says how the system under test should react to them.
-
+
+
+
- Write a label inside each generated file, including the ones that are far too small to hold it
+ a title with its sentence
-
-
-
-
-
-
-
-
-
- Section
-
-
-
-
-
- a section of two rows
-
-
-
-
-
-
-
-
- File configuration
-
-
- Size
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- How many files
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- a section with a line across it
-
-
-
-
-
-
-
-
- Output
-
-
- Size
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- How many files
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- the bar a run starts from
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Preview
-
-
-
-
- Generate
-
-
-
-
-
-
-
-
-
-
-
- Donate
+
+
+
+
+
+ Single batch
-
+
-
-
-
-
-
-
-
-
-
- a section with a long title
-
-
-
-
-
-
-
- Write a label inside each generated file, including the ones that are far too small to hold it
-
-
- Size
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- How many files
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Files of one format and one size, with a manifest that says how the system under test should react to them.
+
+
-
-
-
-
-
-
-
-
-
-
-
- Swatch
-
-
-
-
-
- a surface near the bottom of the ladder
-
-
-
-
-
-
-
-
-
-
-
-
-
- a surface near the top of it
-
-
-
-
-
-
-
-
-
-
-
-
-
- a colour that carries meaning
-
-
-
-
-
-
-
-
-
-
-
-
-
- an ink, which is nearly white
-
-
-
-
-
-
-
-
-
-
-
-
-
- something the toolkit blends, laid over a panel first
-
-
-
-
-
-
-
-
-
-
-
-
-
- Palette, as this window draws it
-
-
-
-
- Every colour the window paints with, measured against what it is drawn on.
-
-
-
-
-
-
-
- The ladder of surfaces
-
-
-
-
-
-
-
-
-
-
-
- background - the page the window stands on
-
-
-
-
-
-
-
-
- #1E1E20
+
+
+
+
+
+ the name of a field
+ Output directory
-
-
-
-
-
-
-
-
- panel - the surface a section is drawn on
+
+
+
+
+
+ the name of a block inside a section
-
-
-
-
- #2B2B2F - 17.7 L*, +6.3 from the page
+
+
+
+
+ Typically finds
-
-
-
-
-
-
-
-
- inputBackground - the fill of a box to type in
+
+
+
+
+
+ a paragraph
-
-
-
-
- #39393F - 24.2 L*, +6.5 from the panel
+
+
+
+
+ Every file has exactly the size you ask for, to the byte, or the run refuses before writing anything.
-
-
-
-
-
-
-
-
- button - the face of a button that is not the main one
+
+
+
+
+
+ a note that has to stay visible
-
-
-
-
- #48484E - 30.8 L*, +6.6 from a box to type in
+
+
+
+
+ The limit of 100 000 files is this tool's, not the disk's.
-
-
-
-
-
-
-
-
- separator - a line that separates and says nothing else
+
+
+
+
+
+ a list of short statements
-
-
-
-
- #48484E - 30.8 L*, +13.1 from the panel
-
+
+
+ •
+
+
+
+
+ Upload validators
+
+
+
-
-
-
-
-
-
-
-
-
-
- menuBackground - what an open list floats on
-
+
+ •
+
+
+
+
+ Size limits
+
+
+
-
-
-
-
-
- #57575F - 37.2 L*, +13.1 from a box to type in
-
+
+ •
+
+
+
+
+ Archive handling
+
+
+
-
+
-
-
-
-
-
-
-
-
- inputBorder - the edge that says where the typing goes
+
+
+
+
+
+ a list whose items do not fit on one line
-
-
-
-
- #65656D - 43.0 L*, +18.9 from its own fill
-
+
+
+ •
+
+
+
+
+ One line
+
+
+
-
-
-
-
-
-
-
- The inks
-
-
-
-
-
-
-
-
-
-
-
- foreground - a value, and everything read at rest
-
-
-
-
-
-
-
-
- #E6E6E8 - 5.74:1 on an open list
-
-
-
-
-
-
-
-
-
-
-
-
-
- label - the name of a field, a step quieter than its value
-
-
-
-
-
-
-
-
- #D3D3D7 - 9.45:1 on a panel
-
-
-
-
-
-
-
-
-
-
-
-
-
- disabled - a value in a box switched off for the length of a run
-
-
-
-
-
-
-
-
- #C7C7CC - 6.81:1 on a box to type in
-
+
+ •
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too small to hold it Write a label inside each
+ generated file, including the ones that are far too small to hold it
+
+
+
-
+
-
-
-
-
-
-
-
-
- placeholder - a hint in a box nobody has typed in
+
+
+
+
+
+ a caption, the smallest rank
-
-
-
-
- #A2A2A9 - 4.52:1 on a box to type in
+
+
+
+
+ 10 485 760 B
-
-
-
-
-
- What carries meaning
-
-
-
-
-
-
-
-
-
-
-
- primary - the action a screen is for, and the line round a focused control
+
+
+
+
+
+ a table of three columns, the last one wrapping
-
-
-
-
- #5AB3F2 - 7.26:1 on the page
-
+
+
+
+
+
+
+ fyne.io/fyne/v2
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- hyperlink - a link, the same blue as the action
-
+
+
+
+
+ BSD-3-Clause
+
+
+
-
-
-
-
-
-
- #5AB3F2 - 7.26:1 on the page
-
+
+
+
+
+ (C) 2018 Fyne.io developers (see
+ AUTHORS)
+
+
+
-
-
-
-
-
-
-
-
-
-
- error - a refusal, and a file that failed
-
+
+
+
+
+
+ Mesa 3D, llvmpipe software
+ renderer 26.2.0
+
+
+
-
-
-
-
-
-
- #F48F8D - 7.26:1 on the page
-
+
+
+
+
+ MIT AND Apache-2.0 WITH
+ LLVM-exception AND BSL-1.0
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- success - a run that wrote everything it promised
-
+
+
+
+
+ Copyright (C) 1999-2007 Brian Paul,
+ Copyright (C) 2008 VMware, Inc.
+
+
+
-
-
-
-
-
- #42C260 - 7.23:1 on the page
-
+
+
+
+
+
+ golang.org/x/text
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- warning - a run that wrote files and skipped some
-
+
+
+
+
+ BSD-3-Clause
+
+
+
-
-
-
-
-
-
- #DAA131 - 7.23:1 on the page
-
+
+
+
+
+ Write a label inside each generated
+ file, including the ones that are far too
+ small to hold it
+
+
+
-
+
-
-
-
-
-
-
-
-
- selection - the row or the segment somebody chose
+
+
+
+
+
+ a long line at every rank
-
-
-
-
- #224B69 - 30.4 L*, +19.0 from the page
+
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too sma…
+
-
-
-
-
-
-
-
-
-
- What the pointer, a press and the keyboard do
+
-
-
-
-
-
-
-
-
-
-
- hover - what the pointer does to a dark face
-
-
-
-
-
-
-
-
- #FFFFFF at 0x22 - over the page it comes to #3C3C3D, 25.4 L*, a move of +14.0
-
-
-
-
-
-
-
-
-
-
-
-
-
- pressed - what a press does to a dark face
-
-
-
-
-
-
-
-
- #FFFFFF at 0x40 - over a button's face it comes to #76767A, 49.7 L*, a move of +19.0
+ Write a label inside each generated file, including the ones that are far too small to hold it
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too small to hold it
+
-
-
-
-
-
-
-
-
-
-
-
-
- lift - what the pointer does to the filled main button
+
+
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too small to hold it
+
-
-
-
-
-
-
-
- #FFFFFF at 0x66 - over the main button it comes to #9CD2F8, 81.6 L*, a move of +11.6
+
+
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too small to hold it
+
-
-
-
+
+
+
-
-
-
-
-
-
-
- shade - what a press does to the filled main button
+
+
+
+
+
+
+
+ Section
+
+
+
+
+
+
+ a section of two rows
-
-
-
-
- #000000 at 0x33 - over the main button it comes to #488FC2, 57.0 L*, a move of -13.0
-
-
+
+
+
+
+ File configuration
+
+
+
+
+ Size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ How many files
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
- focus - the wash the toolkit lays over a control the keyboard is in
+
+
+
+
+
+ a section with a line across it
-
-
-
-
- #00A5F0 at 0x66 - over a box to type in it comes to #226486, 39.9 L*, a move of +15.8
-
-
+
+
+
+
+ Output
+
+
+
+
+ Size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ How many files
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
- tipshade - the shade an explanation casts on the form under it
+
+
+
+
+
+ the bar a run starts from
-
-
-
-
- #000000 at 0x66 - over a panel it comes to #19191C, 9.3 L*, a move of -8.4
-
-
+
+
+
+
+
+
+
+
+
+
+
+ Preview
+
+
+
+
+ Generate
+
+
+
+
+
+
+
+
+
+
+ Donate
+
+
+
+
-
+
-
-
-
-
-
-
-
-
- shadow - what a popup casts - nothing here, on purpose
-
-
-
-
-
-
-
-
- nothing - drawn transparent on purpose
+
+
+
+
+
+ fields in columns, each as wide as its value needs
+
+
+
+
+ File configuration
+
+
+
+
+ Format
+
+
+
+
+
+
+
+ (Select one)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ How many files
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Damage
+
+
+
+
+
+
+
+ (Select one)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Output directory
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- What is written on a filled face
-
-
-
-
-
-
-
-
-
-
-
- foregroundOnPrimary - the word on the main button
+
+
+
+
+
+ a row with a refusal under it
-
-
-
-
- #1A1A1A - 7.60:1 on the main button
-
-
+
+
+
+
+ File configuration
+
+
+
+
+ Size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ size 3 B is below the smallest png, which is 73 B
+
+
+
+
+
+
+
+
+
+ How many files
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too small to
+ hold it
+
+
+
+
+
+
+
+
+
+ Seed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
- foregroundOnError - what is written on a filled red
+
+
+
+
+
+ a section with a long title
-
-
-
-
- #1A1A1A - 7.59:1 on a filled red
+
+
+
+
+ Write a label inside each generated file, including the ones that are far too small to hold it
+
+
+
+
+ Size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ How many files
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Swatch
+
+
+
+
+
+
+ a surface near the bottom of the ladder
+
+
+
-
-
-
-
-
-
-
-
- foregroundOnSuccess - what is written on a filled green
+
+
+
+
+
+ a surface near the top of it
-
-
-
-
- #1A1A1A - 7.56:1 on a filled green
+
+
+
+
+
+
+
+
+
+ a colour that carries meaning
+
+
+
-
-
-
-
-
-
-
-
- foregroundOnWarning - what is written on a filled amber
+
+
+
+
+
+ an ink, which is nearly white
-
-
-
-
- #1A1A1A - 7.56:1 on a filled amber
+
+
+
+
+
+
+
+
+
+ something the toolkit blends, laid over a panel first
+
+
+
-
+
- Palette, light - computed and not installed
-
-
-
-
- Worked out and guarded to the same thresholds. The window does not use it: this program has one look.
+ Palette, as this window draws it
+
+
+
+
+
+ Every colour the window paints with, measured against what it is drawn on.
+
-
-
-
-
-
-
-
- The ladder of surfaces
+
+
+
+
+
+
+ The ladder of surfaces
+
-
-
-
-
-
-
-
-
-
-
- background - the page the window stands on
+
+
+
+
+
+
+
+
+
+ background - the page the window stands on
+
-
-
-
-
-
-
-
- #FFFFFF
+
+
+
+
+
+
+ #1E1E20
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- panel - the surface a section is drawn on
+
+
+
+
+
+
+
+ panel - the surface a section is drawn on
+
-
-
-
-
-
-
-
- #EEEEEF - 94.1 L*, -5.9 from the page
+
+
+
+
+
+
+ #2B2B2F - 17.7 L*, +6.3 from the page
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- inputBackground - the fill of a box to type in
+
+
+
+
+
+
+
+ inputBackground - the fill of a box to type in
+
-
-
-
-
-
-
-
- #DCDCE0 - 87.9 L*, -6.3 from the panel
+
+
+
+
+
+
+ #39393F - 24.2 L*, +6.5 from the panel
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- button - the face of a button that is not the main one
+
+
+
+
+
+
+
+ button - the face of a button that is not the main one
+
-
-
-
-
-
-
-
- #CCCBD1 - 81.9 L*, -5.9 from a box to type in
+
+
+
+
+
+
+ #48484E - 30.8 L*, +6.6 from a box to type in
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- separator - a line that separates and says nothing else
+
+
+
+
+
+
+
+ separator - a line that separates and says nothing else
+
-
-
-
-
-
-
-
- #CCCBD1 - 81.9 L*, -12.2 from the panel
+
+
+
+
+
+
+ #48484E - 30.8 L*, +13.1 from the panel
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- menuBackground - what an open list floats on
+
+
+
+
+
+
+
+ menuBackground - what an open list floats on
+
-
-
-
-
-
-
-
- #FFFFFF - 100.0 L*, +12.1 from a box to type in
+
+
+
+
+
+
+ #57575F - 37.2 L*, +13.1 from a box to type in
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- inputBorder - the edge that says where the typing goes
+
+
+
+
+
+
+
+ inputBorder - the edge that says where the typing goes
+
-
-
-
-
-
-
-
- #ABAAB2 - 69.9 L*, -18.0 from its own fill
+
+
+
+
+
+
+ #65656D - 43.0 L*, +18.9 from its own fill
+
-
-
-
-
-
-
-
-
-
- The inks
+
-
+
-
-
-
-
-
-
-
-
- foreground - a value, and everything read at rest
-
-
-
-
-
-
-
-
- #1A1A1C - 17.38:1 on an open list
-
+
+
+
+
+ The inks
-
-
-
-
-
-
-
-
-
-
-
- label - the name of a field, a step quieter than its value
+
+
+
+
+
+
+
+
+
+
+ foreground - a value, and everything read at rest
+
-
-
-
-
-
-
-
- #434349 - 8.47:1 on a panel
+
+
+
+
+
+
+ #E6E6E8 - 5.74:1 on an open list
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- disabled - a value in a box switched off for the length of a run
+
+
+
+
+
+
+
+ label - the name of a field, a step quieter than its value
+
-
-
-
-
-
-
-
- #3D3D42 - 7.90:1 on a box to type in
+
+
+
+
+
+
+ #D3D3D7 - 9.45:1 on a panel
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- placeholder - a hint in a box nobody has typed in
+
+
+
+
+
+
+
+ disabled - a value in a box switched off for the length of a run
+
-
-
-
-
-
-
-
- #585860 - 5.15:1 on a box to type in
+
+
+
+
+
+
+ #C7C7CC - 6.81:1 on a box to type in
+
-
-
-
-
-
-
-
-
-
- What carries meaning
+
-
+
-
-
-
-
-
-
-
-
- primary - the action a screen is for, and the line round a focused control
+
+
+
+
+
+
+
+ placeholder - a hint in a box nobody has typed in
+
-
-
-
-
-
-
-
- #286893 - 6.01:1 on the page
+
+
+
+
+
+
+ #A2A2A9 - 4.52:1 on a box to type in
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- hyperlink - a link, the same blue as the action
-
+
+
+
+
+ What carries meaning
-
-
-
-
-
-
- #286893 - 6.01:1 on the page
+
+
+
+
+
+
+
+
+
+
+ primary - the action a screen is for, and the line round a focused control
+
-
-
-
+
+
+
+
+
+
+ #5AB3F2 - 7.26:1 on the page
+
+
+
+
+
-
-
-
-
-
-
-
-
- error - a refusal, and a file that failed
+
+
+
+
+
+
+
+ hyperlink - a link, the same blue as the action
+
-
-
-
-
-
-
-
- #BB293B - 6.01:1 on the page
+
+
+
+
+
+
+ #5AB3F2 - 7.26:1 on the page
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- success - a run that wrote everything it promised
+
+
+
+
+
+
+
+ error - a refusal, and a file that failed
+
-
-
-
-
-
-
-
- #237135 - 6.03:1 on the page
+
+
+
+
+
+
+ #F48F8D - 7.26:1 on the page
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- warning - a run that wrote files and skipped some
+
+
+
+
+
+
+
+ success - a run that wrote everything it promised
+
-
-
-
-
-
-
-
- #805D18 - 6.01:1 on the page
+
+
+
+
+
+
+ #42C260 - 7.23:1 on the page
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- selection - the row or the segment somebody chose
+
+
+
+
+
+
+
+ warning - a run that wrote files and skipped some
+
-
-
-
-
-
-
-
- #C1E0FF - 87.9 L*, -12.1 from the page
+
+
+
+
+
+
+ #DAA131 - 7.23:1 on the page
+
-
-
-
-
-
-
-
-
-
- What the pointer, a press and the keyboard do
+
-
+
-
-
-
-
-
-
-
-
- hover - what the pointer does to a dark face
+
+
+
+
+
+
+
+ selection - the row or the segment somebody chose
+
-
-
-
-
-
-
-
- #000000 at 0x20 - over the page it comes to #DFDFDF, 88.8 L*, a move of -11.2
+
+
+
+
+
+
+ #224B69 - 30.4 L*, +19.0 from the page
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- pressed - what a press does to a dark face
-
+
+
+
+
+ What the pointer, a press and the keyboard do
-
-
-
-
-
-
- #000000 at 0x33 - over a button's face it comes to #A3A3A7, 67.0 L*, a move of -15.0
+
+
+
+
+
+
+
+
+
+
+ hover - what the pointer does to a dark face
+
-
-
-
+
+
+
+
+
+
+ #FFFFFF at 0x22 - over the page it comes to #3C3C3D, 25.4 L*, a move of +14.0
+
+
+
+
+
-
-
-
-
-
-
-
-
- lift - what the pointer does to the filled main button
+
+
+
+
+
+
+
+ pressed - what a press does to a dark face
+
-
-
-
-
-
-
-
- #FFFFFF at 0x66 - over the main button it comes to #7EA5BE, 65.6 L*, a move of +23.7
+
+
+
+
+
+
+ #FFFFFF at 0x40 - over a button's face it comes to #76767A, 49.7 L*, a move of +19.0
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- shade - what a press does to the filled main button
+
+
+
+
+
+
+
+ lift - what the pointer does to the filled main button
+
-
-
-
-
-
-
-
- #000000 at 0x33 - over the main button it comes to #205376, 33.6 L*, a move of -8.3
+
+
+
+
+
+
+ #FFFFFF at 0x66 - over the main button it comes to #9CD2F8, 81.6 L*, a move of +11.6
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- focus - the wash the toolkit lays over a control the keyboard is in
+
+
+
+
+
+
+
+ shade - what a press does to the filled main button
+
-
-
-
-
-
-
-
- #007FBA at 0x66 - over a box to type in it comes to #84B7D1, 71.7 L*, a move of -16.1
+
+
+
+
+
+
+ #000000 at 0x33 - over the main button it comes to #488FC2, 57.0 L*, a move of -13.0
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- tipshade - the shade an explanation casts on the form under it
+
+
+
+
+
+
+
+ focus - the wash the toolkit lays over a control the keyboard is in
+
-
-
-
-
-
-
-
- #000000 at 0x40 - over a panel it comes to #B2B2B3, 72.7 L*, a move of -21.4
+
+
+
+
+
+
+ #00A5F0 at 0x66 - over a box to type in it comes to #226486, 39.9 L*, a move of +15.8
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- shadow - what a popup casts - nothing here, on purpose
+
+
+
+
+
+
+
+ tipshade - the shade an explanation casts on the form under it
+
-
-
-
-
-
-
-
- #000000 at 0x40
+
+
+
+
+
+
+ #000000 at 0x66 - over a panel it comes to #19191C, 9.3 L*, a move of -8.4
+
-
-
-
-
-
-
-
-
-
- What is written on a filled face
+
-
+
-
-
-
-
-
-
-
-
- foregroundOnPrimary - the word on the main button
+
+
+
+
+
+
+
+ shadow - what a popup casts - nothing here, on purpose
+
-
-
-
-
-
-
-
- #FFFFFF - 6.01:1 on the main button
+
+
+
+
+
+
+ nothing - drawn transparent on purpose
+
+
+
+
+
+
+
+
+
+ What is written on a filled face
-
-
+
+
+
+
+
+
+
+
+
+
+ foregroundOnPrimary - the word on the main button
+
+
+
+
+
+
+
+
+ #1A1A1A - 7.60:1 on the main button
+
+
+
+
+
-
-
-
-
-
-
-
-
- foregroundOnError - what is written on a filled red
+
+
+
+
+
+
+
+ foregroundOnError - what is written on a filled red
+
-
-
-
-
-
-
-
- #FFFFFF - 6.01:1 on a filled red
+
+
+
+
+
+
+ #1A1A1A - 7.59:1 on a filled red
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- foregroundOnSuccess - what is written on a filled green
+
+
+
+
+
+
+
+ foregroundOnSuccess - what is written on a filled green
+
-
-
-
-
-
-
-
- #FFFFFF - 6.03:1 on a filled green
+
+
+
+
+
+
+ #1A1A1A - 7.56:1 on a filled green
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
- foregroundOnWarning - what is written on a filled amber
+
+
+
+
+
+
+
+ foregroundOnWarning - what is written on a filled amber
+
-
-
-
-
-
-
-
- #FFFFFF - 6.01:1 on a filled amber
+
+
+
+
+
+
+ #1A1A1A - 7.56:1 on a filled amber
+
-
-
-
+
+
+
-
-
-
-
- Not drawn, and why
-
-
-
-
-
-
- Choice - a value: one row of an open list, read back by a guard
-
+
+
+
+
+ Palette, light - computed and not installed
+
+
+
+
+
+ Worked out and guarded to the same thresholds. The window does not use it: this program has one look.
-
-
- •
-
-
-
-
-
-
- Detail - data: the longer explanation of a field and the sheet it opens on
-
+
+
+
+
+
+
+
+ The ladder of surfaces
-
-
- •
-
-
-
-
-
-
- Field - the model of one row of a form, drawn by Fields
+
+
+
+
+
+
+
+
+
+
+ background - the page the window stands on
+
-
-
-
- •
-
-
-
-
-
-
- Built - a value: what building a field comes to, handed to the registry in one piece
+
+
+
+
+
+
+ #FFFFFF
+
-
-
-
- •
+
+
+
-
-
-
-
-
- Tab - data: one screen and the word that leads to it, drawn by Tabs
+
+
+
+
+
+
+
+ panel - the surface a section is drawn on
+
-
-
-
- •
-
-
-
-
-
-
- Look - an enum: which face a Button wears, every value drawn under Button
+
+
+
+
+
+
+ #EEEEEF - 94.1 L*, -5.9 from the page
+
-
-
-
- •
+
+
+
-
-
-
-
-
- PointerFocus - a piece inside a control, knowing what put the keyboard there - no picture of its own
+
+
+
+
+
+
+
+ inputBackground - the fill of a box to type in
+
-
-
-
- •
-
-
-
-
-
-
- Returnable - an interface: a control the window can tell it is coming back to the front, nothing of its own to draw
+
+
+
+
+
+
+ #DCDCE0 - 87.9 L*, -6.3 from the panel
+
-
-
-
- •
+
+
+
-
-
-
-
-
- Shortcuts - a keyboard map: nothing to draw, and saying so is the point of this row
+
+
+
+
+
+
+
+ button - the face of a button that is not the main one
+
-
-
-
- •
-
-
-
-
-
-
-
-
-
- Layout only, and why
-
-
-
-
-
-
- Column - stacks children at one gap of the scale
+
+
+
+
+
+
+ #CCCBD1 - 81.9 L*, -5.9 from a box to type in
+
-
-
-
- •
+
+
+
-
-
-
-
-
- Row - puts fields side by side
+
+
+
+
+
+
+
+ separator - a line that separates and says nothing else
+
-
-
-
- •
-
-
-
-
-
-
- FieldColumn - the column a screen refills at run time, stacked like a section
+
+
+
+
+
+
+ #CCCBD1 - 81.9 L*, -12.2 from the panel
+
-
-
-
- •
+
+
+
-
-
-
-
-
- Indented - the same left edge as the fields inside a panel
+
+
+
+
+
+
+
+ menuBackground - what an open list floats on
+
-
-
-
- •
-
-
-
-
-
-
- Sized - one width, given
+
+
+
+
+
+
+ #FFFFFF - 100.0 L*, +12.1 from a box to type in
+
-
-
-
- •
+
+
+
-
-
-
-
-
- Numeric - the width a number needs
+
+
+
+
+
+
+
+ inputBorder - the edge that says where the typing goes
+
-
-
-
- •
-
-
-
-
-
-
- Text - the width a short piece of text needs - a name, a template, a file name
+
+
+
+
+
+
+ #ABAAB2 - 69.9 L*, -18.0 from its own fill
+
-
-
-
- •
+
+
+
-
-
-
-
-
- Padded - one distance of the scale round its content
-
+
+
+
+
+ The inks
-
-
- •
-
-
-
-
-
-
- Stacked - panels one under another
+
+
+
+
+
+
+
+
+
+
+ foreground - a value, and everything read at rest
+
-
-
-
- •
-
-
-
-
-
-
- Screen - the readable width round a head and its sections
+
+
+
+
+
+
+ #1A1A1C - 17.38:1 on an open list
+
-
-
-
- •
+
+
+
-
-
-
-
-
- BesideFields - the room to the right of the column of names
+
+
+
+
+
+
+
+ label - the name of a field, a step quieter than its value
+
-
-
-
- •
-
-
-
-
-
-
- WithRoomForARun - the room under a form for the bar to speak into
+
+
+
+
+
+
+ #434349 - 8.47:1 on a panel
+
-
-
-
- •
+
+
+
-
-
-
-
-
- Flush - a label kept by a screen, on the edge every other word stands on
+
+
+
+
+
+
+
+ disabled - a value in a box switched off for the length of a run
+
-
-
-
- •
-
-
-
-
-
-
- Clear - a shape that takes room and draws nothing
+
+
+
+
+
+
+ #3D3D42 - 7.90:1 on a box to type in
+
+
+
+
+
+
+
+
+
+
+
+
+
+ placeholder - a hint in a box nobody has typed in
+
+
+
+
+
+
+
+ #585860 - 5.15:1 on a box to type in
+
+
+
+
+
+
+
+
+
+
+ What carries meaning
-
-
- •
+
+
+
+
+
+
+
+
+
+
+ primary - the action a screen is for, and the line round a focused control
+
+
+
+
+
+
+
+
+ #286893 - 6.01:1 on the page
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hyperlink - a link, the same blue as the action
+
+
+
+
+
+
+
+
+ #286893 - 6.01:1 on the page
+
+
+
+
+
+
+
+
+
+
+
+
+
+ error - a refusal, and a file that failed
+
+
+
+
+
+
+
+
+ #BB293B - 6.01:1 on the page
+
+
+
+
+
+
+
+
+
+
+
+
+
+ success - a run that wrote everything it promised
+
+
+
+
+
+
+
+
+ #237135 - 6.03:1 on the page
+
+
+
+
+
+
+
+
+
+
+
+
+
+ warning - a run that wrote files and skipped some
+
+
+
+
+
+
+
+
+ #805D18 - 6.01:1 on the page
+
+
+
+
+
+
+
+
+
+
+
+
+
+ selection - the row or the segment somebody chose
+
+
+
+
+
+
+
+
+ #C1E0FF - 87.9 L*, -12.1 from the page
+
+
+
+
+
+
+
+
+
+
+ What the pointer, a press and the keyboard do
+
+
+
+
+
+
+
+
+
+
+
+ hover - what the pointer does to a dark face
+
+
+
+
+
+
+
+
+ #000000 at 0x20 - over the page it comes to #DFDFDF, 88.8 L*, a move of -11.2
+
+
+
+
+
+
+
+
+
+
+
+
+
+ pressed - what a press does to a dark face
+
+
+
+
+
+
+
+
+ #000000 at 0x33 - over a button's face it comes to #A3A3A7, 67.0 L*, a move of -15.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+ lift - what the pointer does to the filled main button
+
+
+
+
+
+
+
+
+ #FFFFFF at 0x66 - over the main button it comes to #7EA5BE, 65.6 L*, a move of +23.7
+
+
+
+
+
+
+
+
+
+
+
+
+
+ shade - what a press does to the filled main button
+
+
+
+
+
+
+
+
+ #000000 at 0x33 - over the main button it comes to #205376, 33.6 L*, a move of -8.3
+
+
+
+
+
+
+
+
+
+
+
+
+
+ focus - the wash the toolkit lays over a control the keyboard is in
+
+
+
+
+
+
+
+
+ #007FBA at 0x66 - over a box to type in it comes to #84B7D1, 71.7 L*, a move of -16.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tipshade - the shade an explanation casts on the form under it
+
+
+
+
+
+
+
+
+ #000000 at 0x40 - over a panel it comes to #B2B2B3, 72.7 L*, a move of -21.4
+
+
+
+
+
+
+
+
+
+
+
+
+
+ shadow - what a popup casts - nothing here, on purpose
+
+
+
+
+
+
+
+
+ #000000 at 0x40
+
+
+
+
+
+
+
+
+
+
+ What is written on a filled face
+
+
+
+
+
+
+
+
+
+
+
+ foregroundOnPrimary - the word on the main button
+
+
+
+
+
+
+
+
+ #FFFFFF - 6.01:1 on the main button
+
+
+
+
+
+
+
+
+
+
+
+
+
+ foregroundOnError - what is written on a filled red
+
+
+
+
+
+
+
+
+ #FFFFFF - 6.01:1 on a filled red
+
+
+
+
+
+
+
+
+
+
+
+
+
+ foregroundOnSuccess - what is written on a filled green
+
+
+
+
+
+
+
+
+ #FFFFFF - 6.03:1 on a filled green
+
+
+
+
+
+
+
+
+
+
+
+
+
+ foregroundOnWarning - what is written on a filled amber
+
+
+
+
+
+
+
+
+ #FFFFFF - 6.01:1 on a filled amber
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Not drawn, and why
+
+
+
+ •
+
+
+
+
+ Choice - a value: one row of an open list, read back by a guard
+
+
+
+
+
+
+ •
+
+
+
+
+ Detail - data: the longer explanation of a field and the sheet it opens on
+
+
+
+
+
+
+ •
+
+
+
+
+ Field - the model of one row of a form, drawn by Fields
+
+
+
+
+
+
+ •
+
+
+
+
+ Built - a value: what building a field comes to, handed to the registry in one piece
+
+
+
+
+
+
+ •
+
+
+
+
+ Tab - data: one screen and the word that leads to it, drawn by Tabs
+
+
+
+
+
+
+ •
+
+
+
+
+ Look - an enum: which face a Button wears, every value drawn under Button
+
+
+
+
+
+
+ •
+
+
+
+
+ PointerFocus - a piece inside a control, knowing what put the keyboard there - no picture of its own
+
+
+
+
+
+
+ •
+
+
+
+
+ Returnable - an interface: a control the window can tell it is coming back to the front, nothing of its own to draw
+
+
+
+
+
+
+ •
+
+
+
+
+ Shortcuts - a keyboard map: nothing to draw, and saying so is the point of this row
+
+
+
+
+
+
+ •
+
+
+
+
+ GroupKind - an enum: what a group of settings is about, every value drawn under Folding as the colour of its rail
+
+
+
+
+
+
+ •
+
+
+
+
+ NameTap - an invisible target over the name beside a box to tick, so pressing the name ticks the box - nothing of its
+ own to draw
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Layout only, and why
+
+
+
+ •
+
+
+
+
+ Column - stacks children at one gap of the scale
+
+
+
+
+
+
+ •
+
+
+
+
+ Row - puts fields side by side
+
+
+
+
+
+
+ •
+
+
+
+
+ FieldColumn - the column a screen refills at run time, stacked like a section
+
+
+
+
+
+
+ •
+
+
+
+
+ Indented - the same left edge as the fields inside a panel
+
+
+
+
+
+
+ •
+
+
+
+
+ Sized - one width, given
+
+
+
+
+
+
+ •
+
+
+
+
+ Numeric - the width a number needs
+
+
+
+
+
+
+ •
+
+
+
+
+ Text - the width a short piece of text needs - a name, a template, a file name
+
+
+
+
+
+
+ •
+
+
+
+
+ Padded - one distance of the scale round its content
+
+
+
+
+
+
+ •
+
+
+
+
+ Stacked - panels one under another
+
+
+
+
+
+
+ •
+
+
+
+
+ Screen - the readable width round a head and its sections
+
+
+
+
+
+
+ •
+
+
+
+
+ BesideFields - the room to the right of the column of names
+
+
+
+
+
+
+ •
+
+
+
+
+ WithRoomForARun - the room under a form for the bar to speak into
+
+
+
+
+
+
+ •
+
+
+
+
+ Flush - a label kept by a screen, on the edge every other word stands on
+
+
+
+
+
+
+ •
+
+
+
+
+ Clear - a shape that takes room and draws nothing
+
+
+
+
+
+
+ •
+
+
+
+
+ Grid - lays a section's fields in columns, each taking the columns its value needs - drawn under Section
+
+
+
+
+
+
+ •
+
+
+
+
+ Wide - marks something that takes a whole row of a Grid
+
+
+
+
+
+
+ •
+
+
+
+
+ ButtonRow - buttons side by side, one gap of the scale apart and centred - drawn under Section
+
+
+
+
+
diff --git a/internal/guard/testdata/screens/generate-chosen-by-key.png b/internal/guard/testdata/screens/generate-chosen-by-key.png
index 68e5b48..af20412 100644
Binary files a/internal/guard/testdata/screens/generate-chosen-by-key.png and b/internal/guard/testdata/screens/generate-chosen-by-key.png differ
diff --git a/internal/guard/testdata/screens/generate-chosen-by-key.xml b/internal/guard/testdata/screens/generate-chosen-by-key.xml
index 48d2bc3..2b3178b 100644
--- a/internal/guard/testdata/screens/generate-chosen-by-key.xml
+++ b/internal/guard/testdata/screens/generate-chosen-by-key.xml
@@ -56,314 +56,381 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- png
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ png
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for png
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for png
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -380,18 +447,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -412,11 +477,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/generate-chosen.png b/internal/guard/testdata/screens/generate-chosen.png
index 45ce449..38030ae 100644
Binary files a/internal/guard/testdata/screens/generate-chosen.png and b/internal/guard/testdata/screens/generate-chosen.png differ
diff --git a/internal/guard/testdata/screens/generate-chosen.xml b/internal/guard/testdata/screens/generate-chosen.xml
index e310030..c299131 100644
--- a/internal/guard/testdata/screens/generate-chosen.xml
+++ b/internal/guard/testdata/screens/generate-chosen.xml
@@ -56,314 +56,381 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- png
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ png
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for png
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for png
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -380,18 +447,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -412,11 +477,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/generate-empty.png b/internal/guard/testdata/screens/generate-empty.png
index 573d3b8..060010b 100644
Binary files a/internal/guard/testdata/screens/generate-empty.png and b/internal/guard/testdata/screens/generate-empty.png differ
diff --git a/internal/guard/testdata/screens/generate-empty.xml b/internal/guard/testdata/screens/generate-empty.xml
index fbabf01..23aad1b 100644
--- a/internal/guard/testdata/screens/generate-empty.xml
+++ b/internal/guard/testdata/screens/generate-empty.xml
@@ -56,326 +56,393 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- 0
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
-
-
-
+
+
-
-
-
-
-
-
- target "files" asks for 0 files. Ask for at least one
+
+
+
+
+
+ target "files" asks for 0 files. Ask for at least one
+
+
+
+
+
+
+
+
+ Damage
+
+
+
+
-
-
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -392,18 +459,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -424,11 +489,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/generate-focused.png b/internal/guard/testdata/screens/generate-focused.png
index d1dc30c..6ec51a4 100644
Binary files a/internal/guard/testdata/screens/generate-focused.png and b/internal/guard/testdata/screens/generate-focused.png differ
diff --git a/internal/guard/testdata/screens/generate-focused.xml b/internal/guard/testdata/screens/generate-focused.xml
index 4a857b6..e278d7c 100644
--- a/internal/guard/testdata/screens/generate-focused.xml
+++ b/internal/guard/testdata/screens/generate-focused.xml
@@ -56,315 +56,382 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -381,18 +448,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -413,11 +478,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/generate-hovered.png b/internal/guard/testdata/screens/generate-hovered.png
index 89f1de3..cf89094 100644
Binary files a/internal/guard/testdata/screens/generate-hovered.png and b/internal/guard/testdata/screens/generate-hovered.png differ
diff --git a/internal/guard/testdata/screens/generate-hovered.xml b/internal/guard/testdata/screens/generate-hovered.xml
index dffef6d..e7108ed 100644
--- a/internal/guard/testdata/screens/generate-hovered.xml
+++ b/internal/guard/testdata/screens/generate-hovered.xml
@@ -56,314 +56,381 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -380,18 +447,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -412,11 +477,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
@@ -425,7 +490,7 @@
-
+
diff --git a/internal/guard/testdata/screens/generate-menu-hovered.png b/internal/guard/testdata/screens/generate-menu-hovered.png
index 96e919a..b50344e 100644
Binary files a/internal/guard/testdata/screens/generate-menu-hovered.png and b/internal/guard/testdata/screens/generate-menu-hovered.png differ
diff --git a/internal/guard/testdata/screens/generate-menu-hovered.xml b/internal/guard/testdata/screens/generate-menu-hovered.xml
index ae5e82b..73a1e2d 100644
--- a/internal/guard/testdata/screens/generate-menu-hovered.xml
+++ b/internal/guard/testdata/screens/generate-menu-hovered.xml
@@ -56,314 +56,381 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -380,18 +447,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -412,11 +477,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
@@ -431,262 +496,262 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
- avif
+ avif
-
-
-
+
+
+
- bmp
+ bmp
-
-
-
+
+
+
- csv
+ csv
-
-
-
+
+
+
- docx
+ docx
-
-
-
+
+
+
- gif
+ gif
-
-
-
+
+
+
- html
+ html
-
-
-
+
+
+
- ico
+ ico
-
-
-
+
+
+
- jpg
+ jpg
-
-
-
+
+
+
- json
+ json
-
-
-
+
+
+
- jxl
+ jxl
-
-
-
+
+
+
- log
+ log
-
-
-
+
+
+
- md
+ md
-
-
-
+
+
+
- pdf
+ pdf
-
-
-
+
+
+
- png
+ png
-
-
-
+
+
+
- pptx
+ pptx
-
-
-
+
+
+
- svg
+ svg
-
-
-
+
+
+
- targz
+ targz
-
-
-
+
+
+
- tiff
+ tiff
-
-
-
+
+
+
- toml
+ toml
-
-
-
+
+
+
- txt
+ txt
-
-
-
+
+
+
- wav
+ wav
-
-
-
+
+
+
- webp
+ webp
-
-
-
+
+
+
- xlsx
+ xlsx
-
-
-
+
+
+
- xml
+ xml
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
diff --git a/internal/guard/testdata/screens/generate-menu-keyed.png b/internal/guard/testdata/screens/generate-menu-keyed.png
index 64cf0b1..bbb1520 100644
Binary files a/internal/guard/testdata/screens/generate-menu-keyed.png and b/internal/guard/testdata/screens/generate-menu-keyed.png differ
diff --git a/internal/guard/testdata/screens/generate-menu-keyed.xml b/internal/guard/testdata/screens/generate-menu-keyed.xml
index 37408b8..ceee587 100644
--- a/internal/guard/testdata/screens/generate-menu-keyed.xml
+++ b/internal/guard/testdata/screens/generate-menu-keyed.xml
@@ -56,314 +56,381 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -380,18 +447,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -412,11 +477,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
@@ -431,262 +496,262 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
- avif
+ avif
-
-
-
+
+
+
- bmp
+ bmp
-
-
-
+
+
+
- csv
+ csv
-
-
-
+
+
+
- docx
+ docx
-
-
-
+
+
+
- gif
+ gif
-
-
-
+
+
+
- html
+ html
-
-
-
+
+
+
- ico
+ ico
-
-
-
+
+
+
- jpg
+ jpg
-
-
-
+
+
+
- json
+ json
-
-
-
+
+
+
- jxl
+ jxl
-
-
-
+
+
+
- log
+ log
-
-
-
+
+
+
- md
+ md
-
-
-
+
+
+
- pdf
+ pdf
-
-
-
+
+
+
- png
+ png
-
-
-
+
+
+
- pptx
+ pptx
-
-
-
+
+
+
- svg
+ svg
-
-
-
+
+
+
- targz
+ targz
-
-
-
+
+
+
- tiff
+ tiff
-
-
-
+
+
+
- toml
+ toml
-
-
-
+
+
+
- txt
+ txt
-
-
-
+
+
+
- wav
+ wav
-
-
-
+
+
+
- webp
+ webp
-
-
-
+
+
+
- xlsx
+ xlsx
-
-
-
+
+
+
- xml
+ xml
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
diff --git a/internal/guard/testdata/screens/generate-menu.png b/internal/guard/testdata/screens/generate-menu.png
index fdc15a9..b8ed6b7 100644
Binary files a/internal/guard/testdata/screens/generate-menu.png and b/internal/guard/testdata/screens/generate-menu.png differ
diff --git a/internal/guard/testdata/screens/generate-menu.xml b/internal/guard/testdata/screens/generate-menu.xml
index 3273c33..170f5a6 100644
--- a/internal/guard/testdata/screens/generate-menu.xml
+++ b/internal/guard/testdata/screens/generate-menu.xml
@@ -56,314 +56,381 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -380,18 +447,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -412,11 +477,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
@@ -431,262 +496,262 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
- avif
+ avif
-
-
-
+
+
+
- bmp
+ bmp
-
-
-
+
+
+
- csv
+ csv
-
-
-
+
+
+
- docx
+ docx
-
-
-
+
+
+
- gif
+ gif
-
-
-
+
+
+
- html
+ html
-
-
-
+
+
+
- ico
+ ico
-
-
-
+
+
+
- jpg
+ jpg
-
-
-
+
+
+
- json
+ json
-
-
-
+
+
+
- jxl
+ jxl
-
-
-
+
+
+
- log
+ log
-
-
-
+
+
+
- md
+ md
-
-
-
+
+
+
- pdf
+ pdf
-
-
-
+
+
+
- png
+ png
-
-
-
+
+
+
- pptx
+ pptx
-
-
-
+
+
+
- svg
+ svg
-
-
-
+
+
+
- targz
+ targz
-
-
-
+
+
+
- tiff
+ tiff
-
-
-
+
+
+
- toml
+ toml
-
-
-
+
+
+
- txt
+ txt
-
-
-
+
+
+
- wav
+ wav
-
-
-
+
+
+
- webp
+ webp
-
-
-
+
+
+
- xlsx
+ xlsx
-
-
-
+
+
+
- xml
+ xml
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
diff --git a/internal/guard/testdata/screens/generate-refused-both.png b/internal/guard/testdata/screens/generate-refused-both.png
index 15365ed..d80a421 100644
Binary files a/internal/guard/testdata/screens/generate-refused-both.png and b/internal/guard/testdata/screens/generate-refused-both.png differ
diff --git a/internal/guard/testdata/screens/generate-refused-both.xml b/internal/guard/testdata/screens/generate-refused-both.xml
index f5eccac..1040631 100644
--- a/internal/guard/testdata/screens/generate-refused-both.xml
+++ b/internal/guard/testdata/screens/generate-refused-both.xml
@@ -56,337 +56,405 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- abc
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ abc
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+ Size "abc" has no number: write something like 10mb or 1048576
+
+
+
+
+
+
+
+
+ How many files
+
+ *
-
-
-
-
-
-
-
- Size "abc" has no number: write something like 10mb or 1048576
+
+
+
+
+
+
+
+
+ many
+
+
+
-
+
+
-
-
-
-
-
-
- How many files
-
- *
-
-
-
-
-
-
-
-
-
- many
-
+
+
+
+
+
+ How many files is "many", which is not a whole number.
+ Write the digits out, such as 1 or 500
+
+
+
+
+
+
+
+ Damage
+
+
+
+
-
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
-
-
-
-
-
- How many files is "many", which is not a whole number. Write the digits out, such as 1 or 500
-
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -403,18 +471,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -435,11 +501,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/generate-refused-setting.png b/internal/guard/testdata/screens/generate-refused-setting.png
index 4aee4dd..0fb0f78 100644
Binary files a/internal/guard/testdata/screens/generate-refused-setting.png and b/internal/guard/testdata/screens/generate-refused-setting.png differ
diff --git a/internal/guard/testdata/screens/generate-refused-setting.xml b/internal/guard/testdata/screens/generate-refused-setting.xml
index 705729f..aa6f73e 100644
--- a/internal/guard/testdata/screens/generate-refused-setting.xml
+++ b/internal/guard/testdata/screens/generate-refused-setting.xml
@@ -32,8 +32,8 @@
-
-
+
+
@@ -56,396 +56,463 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- png
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ png
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
-
-
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+
+
+
+
+ 10mb
+
-
-
+
+
-
-
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for png
-
-
-
-
-
-
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
-
-
+
+
+
-
-
-
-
- Width
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 99999
-
-
-
+
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
-
-
-
-
-
-
-
-
-
-
- png: Width cannot be "99999" - it takes a whole number of pixels from 1 to 20000
+
+
-
+
+
-
-
-
- Height
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- worked out from the size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Width
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+ 99999
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ png: Width cannot be "99999" - it takes a whole number of pixels from 1 to 20000
+
+
+
+
+
+
+
+
+
+ Height
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ worked out from the size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ width times height cannot pass 40 megapixels, because the picture is held in memory while it is encoded
-
+
-
-
-
-
-
- width times height cannot pass 40 megapixels, because the picture is held in memory while it is encoded
-
-
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -453,14 +520,6 @@
-
-
-
-
-
-
-
-
@@ -470,18 +529,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -502,11 +559,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/generate-refused.png b/internal/guard/testdata/screens/generate-refused.png
index 25bbcbb..653e68e 100644
Binary files a/internal/guard/testdata/screens/generate-refused.png and b/internal/guard/testdata/screens/generate-refused.png differ
diff --git a/internal/guard/testdata/screens/generate-refused.xml b/internal/guard/testdata/screens/generate-refused.xml
index 14148ab..bd9deb6 100644
--- a/internal/guard/testdata/screens/generate-refused.xml
+++ b/internal/guard/testdata/screens/generate-refused.xml
@@ -56,328 +56,403 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
-
-
-
+
+
+
+
+
+
+ 1 B
+
+
+
+
-
-
-
-
- 1 B
+
+
+
+
+
+ AVIF cannot be smaller than 311 B - the smallest picture this format draws codes to 303
+ B at worst, and the file always carries a free box, which costs 8 B even when it holds
+ nothing. Requested: 1 B. Ask for 311 B or more, or set a smaller width and height, or a
+ lower quality
+
+
+
+
+
+
+
+ Use the smallest size, 311 B
-
+
-
-
-
-
-
- AVIF cannot be smaller than 311 B - the smallest picture this format draws codes to 303 B at worst, and the file always
- carries a free box, which costs 8 B even when it holds nothing. Requested: 1 B. Ask for 311 B or more, or set a smaller
- width and height, or a lower quality
-
+
+
+
+ How many files
+
+ *
-
-
-
-
-
-
- How many files
-
- *
-
-
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -394,18 +469,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -426,11 +499,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/generate-switch-by-key.png b/internal/guard/testdata/screens/generate-switch-by-key.png
index 1547f70..65b6614 100644
Binary files a/internal/guard/testdata/screens/generate-switch-by-key.png and b/internal/guard/testdata/screens/generate-switch-by-key.png differ
diff --git a/internal/guard/testdata/screens/generate-switch-by-key.xml b/internal/guard/testdata/screens/generate-switch-by-key.xml
index 70a8230..f70b1c0 100644
--- a/internal/guard/testdata/screens/generate-switch-by-key.xml
+++ b/internal/guard/testdata/screens/generate-switch-by-key.xml
@@ -56,314 +56,381 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -380,18 +447,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -412,11 +477,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/generate-typed.png b/internal/guard/testdata/screens/generate-typed.png
index 00b3dc4..20cb884 100644
Binary files a/internal/guard/testdata/screens/generate-typed.png and b/internal/guard/testdata/screens/generate-typed.png differ
diff --git a/internal/guard/testdata/screens/generate-typed.xml b/internal/guard/testdata/screens/generate-typed.xml
index 06528c9..c214095 100644
--- a/internal/guard/testdata/screens/generate-typed.xml
+++ b/internal/guard/testdata/screens/generate-typed.xml
@@ -56,325 +56,392 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- abc
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
-
- Size "abc" has no number: write something like 10mb or 1048576
+
+
+
+
+
+
+
+
+ abc
+
+
+
-
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- 1
-
+
+
+
+
+
+ Size "abc" has no number: write something like 10mb or 1048576
+
+
+
+
+
+
+
+ How many files
+
+ *
-
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -391,18 +458,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -423,11 +488,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/generate-unchecked.png b/internal/guard/testdata/screens/generate-unchecked.png
index ff08a9f..93f2e94 100644
Binary files a/internal/guard/testdata/screens/generate-unchecked.png and b/internal/guard/testdata/screens/generate-unchecked.png differ
diff --git a/internal/guard/testdata/screens/generate-unchecked.xml b/internal/guard/testdata/screens/generate-unchecked.xml
index 1c7dd2a..5fd5423 100644
--- a/internal/guard/testdata/screens/generate-unchecked.xml
+++ b/internal/guard/testdata/screens/generate-unchecked.xml
@@ -56,313 +56,380 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
@@ -379,18 +446,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -411,11 +476,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/generate.png b/internal/guard/testdata/screens/generate.png
index 036189e..b2ea022 100644
Binary files a/internal/guard/testdata/screens/generate.png and b/internal/guard/testdata/screens/generate.png differ
diff --git a/internal/guard/testdata/screens/generate.xml b/internal/guard/testdata/screens/generate.xml
index 0bd87f6..1ad1c75 100644
--- a/internal/guard/testdata/screens/generate.xml
+++ b/internal/guard/testdata/screens/generate.xml
@@ -56,314 +56,381 @@
-
-
-
-
- File configuration
-
-
-
- Format
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- avif
+
+
+
+
-
-
+ File configuration
+
+
+
-
-
+
+
+
-
-
-
- Size
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+ Format
+
+
+
+
+
+
+
+
+
+
+
+
+ avif
+
+
+
-
-
+
+
-
-
-
-
- 10 485 760 B
-
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
-
-
-
-
-
-
- How many files
-
- *
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
+
+
+
+
+
+
+
+
+ 10 485 760 B
+
+
+
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+ How many files
+
+ *
+
+
+
+
+
+
+
+
+
+
+ 1
+
-
-
+
+
-
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ Damage
+
+
+
+
+
+
+
+
+
+
+
+
+ none
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- files
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
+
-
-
+
+
-
-
-
-
- File names
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files_0001
-
-
-
+
+
+
+ File names
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files_0001
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
- Damage
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- none
+
+
+
+
-
-
+ Output
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Output
-
+
-
- Output directory
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
-
-
-
- Choose...
-
+
+
+
+ Choose...
+
+
+
+
-
-
-
-
-
- Seed
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -380,18 +447,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -412,11 +477,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/preset-many-settings.png b/internal/guard/testdata/screens/preset-many-settings.png
index 66ac53a..5e99fde 100644
Binary files a/internal/guard/testdata/screens/preset-many-settings.png and b/internal/guard/testdata/screens/preset-many-settings.png differ
diff --git a/internal/guard/testdata/screens/preset-many-settings.xml b/internal/guard/testdata/screens/preset-many-settings.xml
index e1eaae4..3b16f3d 100644
--- a/internal/guard/testdata/screens/preset-many-settings.xml
+++ b/internal/guard/testdata/screens/preset-many-settings.xml
@@ -56,297 +56,347 @@
-
-
-
-
- The question
-
-
-
- Preset
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- upload-validation
+
+
+
+
-
-
+ The question
+
+
+
-
-
+
+
+
-
-
-
-
-
- Does my upload form take what it should and turn the rest away?
-
-
-
-
-
-
-
-
- Typically finds:
+
+
+
+
+ Preset
+
+
+
+
-
-
-
-
-
-
-
-
-
- a limit enforced in the browser and not on the server
+
+
+
+
+
+
+
+ upload-validation
-
-
-
- •
-
-
-
-
-
-
- an SVG or an HTML file taken for a picture or for plain text, which is a way to get a script past a form
+
+
+
-
- •
+
-
-
-
-
-
- a file checked by its extension and never opened, so a PDF named .jpg goes through
-
+
+
+
+
+
+
+ Does my upload form take what it should and turn the rest away?
-
-
- •
-
-
-
-
-
-
- a form that reads the whole body into memory before it looks at how big it is
-
+
+
+
+
+
+
+
+ Typically finds:
-
-
- •
-
-
-
-
-
-
- an upload named PHOTO.JPG turned away where photo.jpg is taken, or the other way round
+
+
+
+
+
+ •
+
+
+
+
+ a limit enforced in the browser and not on the server
+
-
-
-
- •
-
-
-
-
-
-
- a name with spaces, brackets or characters outside ASCII written to disk unchanged
+
+
+
+
+ •
+
+
+
+
+ an SVG or an HTML file taken for a picture or for plain text, which is a way to get a script past a form
+
-
-
-
- •
+
+
+
+
+ •
+
+
+
+
+ a file checked by its extension and never opened, so a PDF named .jpg goes through
+
+
+
+
+
+
+ •
+
+
+
+
+ a form that reads the whole body into memory before it looks at how big it is
+
+
+
+
+
+
+ •
+
+
+
+
+ an upload named PHOTO.JPG turned away where photo.jpg is taken, or the other way round
+
+
+
+
+
+
+ •
+
+
+
+
+ a name with spaces, brackets or characters outside ASCII written to disk unchanged
+
+
+
+
+
-
-
-
-
- Settings
-
-
-
-
- Limit
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+
-
-
-
-
-
-
-
-
- 10mb
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Limit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: 10mb
+
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- Allow
-
-
-
-
+
-
-
-
-
-
-
-
-
- jpg,png,pdf
-
-
-
+
+
+
+
+ Allow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: jpg,png,pdf
+
+
+
+
-
-
+
+
-
-
-
-
- Deny
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- svg,html,exe,sh
-
-
-
+
+
+
+ Deny
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: svg,html,exe,sh
+
+
+
+
-
-
+
+
-
-
-
-
- Far-over
-
-
-
-
-
-
-
-
-
-
-
-
- 2x
-
-
-
-
+
+
+
+ Far-over
+
+
+
+
-
+
+
+
+
+
+
+
+ 2x
+
+
+
+
+
+
+
-
-
-
-
- Bulk
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 50
-
-
-
+
+
+
+ Bulk
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: 50
+
+
+
+
-
-
+
+
@@ -355,74 +405,101 @@
-
-
-
-
- Output
-
-
-
- Output directory
-
- *
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- /tfg/out
-
-
+
+
+
+
-
-
-
-
- Choose...
-
+ Output
+
+
+
+
+
-
+
+
-
+
-
- Seed
-
- *
-
-
-
-
-
-
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
+
+
+
+
+
+ Choose...
+
+
+
+
+
-
-
-
-
-
-
-
-
- 0
+
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
@@ -441,18 +518,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -461,7 +536,7 @@
- 71 files · 115.1 MB (120 639 488 B) · html, jpg, pdf, png, svg, txt · will go to /tfg/out
+ 71 files · 115.1 MB (120 639 488 B) · 6 formats · will go to /tfg/out
@@ -473,11 +548,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/preset-menu-setting.png b/internal/guard/testdata/screens/preset-menu-setting.png
index f0d3832..1dd604c 100644
Binary files a/internal/guard/testdata/screens/preset-menu-setting.png and b/internal/guard/testdata/screens/preset-menu-setting.png differ
diff --git a/internal/guard/testdata/screens/preset-menu-setting.xml b/internal/guard/testdata/screens/preset-menu-setting.xml
index 06bad88..c369f90 100644
--- a/internal/guard/testdata/screens/preset-menu-setting.xml
+++ b/internal/guard/testdata/screens/preset-menu-setting.xml
@@ -56,199 +56,249 @@
-
-
-
-
- The question
-
-
-
- Preset
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- size-boundaries
+
+
+
+
-
-
+ The question
+
+
+
-
-
+
+
+
-
-
-
-
-
- Is a size limit enforced exactly where it is declared?
-
-
-
-
-
-
-
-
- Typically finds:
+
+
+
+
+ Preset
+
+
+
+
-
-
-
-
-
-
-
-
-
- off by one errors at the limit
+
+
+
+
+
+
+
+ size-boundaries
-
-
-
- •
-
-
-
-
-
-
- MB confused with MiB, which is 4.8 per cent and enough to let a file through that should not pass
+
+
+
-
- •
+
-
-
-
-
-
- a limit enforced in the browser and not on the server
-
+
+
+
+
+
+
+ Is a size limit enforced exactly where it is declared?
-
-
- •
+
+
+
+
+
+
+
+ Typically finds:
+
+
+
+
+
+
+ •
+
+
+
+
+ off by one errors at the limit
+
+
+
+
+
+
+ •
+
+
+
+
+ MB confused with MiB, which is 4.8 per cent and enough to let a file through that should not pass
+
+
+
+
+
+
+ •
+
+
+
+
+ a limit enforced in the browser and not on the server
+
+
+
+
+
-
-
-
-
- Settings
-
-
-
-
- Limit
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+
-
-
-
-
-
-
-
-
- 10mb
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Limit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: 10mb
+
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- Spread
-
-
-
-
+
-
-
-
-
-
-
-
-
- 1B,1kb,1mb
-
-
-
+
+
+
+
+ Spread
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: 1B,1kb,1mb
+
+
+
+
-
-
+
+
-
-
-
-
- Format
-
-
-
-
-
-
-
-
-
-
-
-
- pdf
-
-
-
-
+
+
+
+ Format
+
+
+
+
-
+
+
+
+
+
+
+
+ pdf
+
+
+
+
+
+
+
@@ -257,74 +307,101 @@
-
-
-
-
- Output
-
-
-
- Output directory
-
- *
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- /tfg/out
-
-
+
+
+
+
-
-
-
-
- Choose...
-
+ Output
+
+
+
+
+
-
+
+
-
+
-
- Seed
-
- *
-
-
-
-
-
-
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
+
+
+
+
+
+ Choose...
+
+
+
+
+
-
-
-
-
-
-
-
-
- 0
+
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
@@ -343,18 +420,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -375,11 +450,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
@@ -394,234 +469,264 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
- avif
+ avif
-
-
-
+
+
+
- bmp
+ bmp
-
-
-
+
+
+
- csv
+ csv
-
-
-
+
+
+
- docx
+ docx
-
-
-
+
+
+
- gif
+ gif
-
-
-
+
+
+
- html
+ html
-
-
-
+
+
+
- ico
+ ico
-
-
-
+
+
+
- jpg
+ jpg
-
-
-
+
+
+
- json
+ json
-
-
-
+
+
+
- jxl
+ jxl
-
-
-
+
+
+
- log
+ log
-
-
-
+
+
+
- md
+ md
-
-
-
+
+
+
- pdf
+ pdf
-
-
-
+
+
+
- png
+ png
-
-
-
+
+
+
- pptx
+ pptx
-
-
-
+
+
+
- svg
+ svg
-
-
-
+
+
+
- targz
+ targz
-
-
-
+
+
+
- tiff
+ tiff
-
-
-
+
+
+
- toml
+ toml
-
-
-
+
+
+
- txt
+ txt
-
-
-
+
+
+
- wav
+ wav
+
+
+
+
+
+
+ webp
+
+
+
+
+
+
+ xlsx
+
+
+
+
+
+
+ xml
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
diff --git a/internal/guard/testdata/screens/preset-menu.png b/internal/guard/testdata/screens/preset-menu.png
index c680137..0363b63 100644
Binary files a/internal/guard/testdata/screens/preset-menu.png and b/internal/guard/testdata/screens/preset-menu.png differ
diff --git a/internal/guard/testdata/screens/preset-menu.xml b/internal/guard/testdata/screens/preset-menu.xml
index 9006bee..cda8f82 100644
--- a/internal/guard/testdata/screens/preset-menu.xml
+++ b/internal/guard/testdata/screens/preset-menu.xml
@@ -56,144 +56,194 @@
-
-
-
-
- The question
-
-
-
- Preset
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- empty-and-minimal
+
+
+
+
-
-
+ The question
+
+
+
-
-
+
+
+
-
-
-
-
-
- Does a file that is valid and as small as the format allows get through?
-
-
-
-
-
-
-
-
- Typically finds:
+
+
+
+
+ Preset
+
+
+
+
-
-
-
-
-
-
-
-
-
- a valid file turned away for being too small, where the check counts bytes instead of reading them
+
+
+
+
+
+
+
+ empty-and-minimal
-
-
-
- •
-
-
-
-
-
-
- an empty file that brings the reader down rather than being reported
+
+
+
-
- •
+
-
-
-
-
-
- a picture one pixel wide that divides by zero on the way to a thumbnail
-
+
+
+
+
+
+
+ Does a file that is valid and as small as the format allows get through?
-
-
- •
-
-
-
-
-
-
- storage that reads nought bytes as a failed upload and keeps retrying
-
+
+
+
+
+
+
+
+ Typically finds:
-
-
- •
+
+
+
+
+
+ •
+
+
+
+
+ a valid file turned away for being too small, where the check counts bytes instead of reading them
+
+
+
+
+
+
+ •
+
+
+
+
+ an empty file that brings the reader down rather than being reported
+
+
+
+
+
+
+ •
+
+
+
+
+ a picture one pixel wide that divides by zero on the way to a thumbnail
+
+
+
+
+
+
+ •
+
+
+
+
+ storage that reads nought bytes as a failed upload and keeps retrying
+
+
+
+
+
-
-
-
-
- Settings
-
-
-
-
- Formats
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+
-
-
-
-
-
-
-
-
- all
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Formats
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: all
+
+
+
+
-
-
+
+
@@ -202,74 +252,101 @@
-
-
-
-
- Output
-
-
-
- Output directory
-
- *
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
-
-
+
+
+
+
-
-
-
-
- Choose...
-
+ Output
+
+
+
+
+
-
+
+
-
+
-
- Seed
-
- *
-
-
-
-
-
-
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
+
+
+
+
+
+ Choose...
+
+
+
+
+
-
-
-
-
-
-
-
-
- 0
+
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
@@ -288,28 +365,25 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
-
-
-
-
- 28 files · 31.9 KB (32 667 B) · avif, bmp, csv, docx, gif, html, ico, jpg, json, jxl, log, md, pdf, png, pptx, svg, targz, tiff,
- toml, txt, wav, webp, xlsx, xml, yaml, zip · will go to /tfg/out
+
+
+
+
+ 28 files · 31.9 KB (32 667 B) · 26 formats · will go to /tfg/out
@@ -321,11 +395,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
@@ -340,61 +414,61 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- empty-and-minimal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ empty-and-minimal
-
-
-
- size-boundaries
+
+
+
+ size-boundaries
-
-
-
- tabular-import
+
+
+
+ tabular-import
-
-
-
- text-encoding
+
+
+
+ text-encoding
-
-
-
- upload-validation
+
+
+
+ upload-validation
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
diff --git a/internal/guard/testdata/screens/preset-refused.png b/internal/guard/testdata/screens/preset-refused.png
index b9ff8c6..90f677f 100644
Binary files a/internal/guard/testdata/screens/preset-refused.png and b/internal/guard/testdata/screens/preset-refused.png differ
diff --git a/internal/guard/testdata/screens/preset-refused.xml b/internal/guard/testdata/screens/preset-refused.xml
index c89861f..7beea65 100644
--- a/internal/guard/testdata/screens/preset-refused.xml
+++ b/internal/guard/testdata/screens/preset-refused.xml
@@ -56,210 +56,260 @@
-
-
-
-
- The question
-
-
-
- Preset
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- size-boundaries
+
+
+
+
-
-
+ The question
+
+
+
-
-
+
+
+
-
-
-
-
-
- Is a size limit enforced exactly where it is declared?
-
-
-
-
-
-
-
-
- Typically finds:
+
+
+
+
+ Preset
+
+
+
+
-
-
-
-
-
-
-
-
-
- off by one errors at the limit
+
+
+
+
+
+
+
+ size-boundaries
-
-
-
- •
-
-
-
-
-
-
- MB confused with MiB, which is 4.8 per cent and enough to let a file through that should not pass
+
+
+
-
- •
+
-
-
-
-
-
- a limit enforced in the browser and not on the server
-
+
+
+
+
+
+
+ Is a size limit enforced exactly where it is declared?
-
-
- •
+
+
+
+
+
+
+
+ Typically finds:
+
+
+
+
+
+
+ •
+
+
+
+
+ off by one errors at the limit
+
+
+
+
+
+
+ •
+
+
+
+
+ MB confused with MiB, which is 4.8 per cent and enough to let a file through that should not pass
+
+
+
+
+
+
+ •
+
+
+
+
+ a limit enforced in the browser and not on the server
+
+
+
+
+
-
-
-
-
- Settings
-
-
-
-
- Limit
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+
-
-
-
-
-
-
-
-
- 512
+
+
+
+
+
+
+
+
+
+
+ Limit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 512
+
+
-
-
-
+
+
+
+
+
+
+ 512 B
+
+
+
+
-
-
-
-
- 512 B
+
+
+
+
+
+ the preset size-boundaries cannot build this set - under_1mb would be -1048064 B, and a file cannot be smaller than
+ nothing. Raise the Limit above 1051991 B, narrow the spread, or choose a format with a smaller minimum. The Limit
+ asked for was 512 B.
+
-
-
-
+
+
+
-
-
-
-
-
- the preset size-boundaries cannot build this set - under_1mb would be -1048064 B, and a file cannot be smaller than
- nothing. Raise the Limit above 1051991 B, narrow the spread, or choose a format with a smaller minimum. The Limit
- asked for was 512 B.
-
+
+
+
+ Spread
+
+
+
+
-
-
-
-
-
-
- Spread
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1B,1kb,1mb
-
-
-
+
+
+
+
+
+
+
+
+ default: 1B,1kb,1mb
+
+
+
+
-
-
+
+
-
-
-
-
- Format
-
-
-
-
-
-
-
-
-
-
-
-
- pdf
-
-
-
-
+
+
+
+ Format
+
+
+
+
-
+
+
+
+
+
+
+
+ pdf
+
+
+
+
+
+
+
@@ -268,74 +318,101 @@
-
-
-
-
- Output
-
-
-
- Output directory
-
- *
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- /tfg/out
-
-
+
+
+
+
-
-
-
-
- Choose...
-
+ Output
+
+
+
+
+
-
+
+
-
+
-
- Seed
-
- *
-
-
-
-
-
-
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
+
+
+
+
+
+ Choose...
+
+
+
+
+
-
-
-
-
-
-
-
-
- 0
+
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
@@ -354,18 +431,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -386,11 +461,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/preset.png b/internal/guard/testdata/screens/preset.png
index ee40750..1d76902 100644
Binary files a/internal/guard/testdata/screens/preset.png and b/internal/guard/testdata/screens/preset.png differ
diff --git a/internal/guard/testdata/screens/preset.xml b/internal/guard/testdata/screens/preset.xml
index 51a0d9b..a16cefb 100644
--- a/internal/guard/testdata/screens/preset.xml
+++ b/internal/guard/testdata/screens/preset.xml
@@ -56,144 +56,194 @@
-
-
-
-
- The question
-
-
-
- Preset
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- empty-and-minimal
+
+
+
+
-
-
+ The question
+
+
+
-
-
+
+
+
-
-
-
-
-
- Does a file that is valid and as small as the format allows get through?
-
-
-
-
-
-
-
-
- Typically finds:
+
+
+
+
+ Preset
+
+
+
+
-
-
-
-
-
-
-
-
-
- a valid file turned away for being too small, where the check counts bytes instead of reading them
+
+
+
+
+
+
+
+ empty-and-minimal
-
-
-
- •
-
-
-
-
-
-
- an empty file that brings the reader down rather than being reported
+
+
+
-
- •
+
-
-
-
-
-
- a picture one pixel wide that divides by zero on the way to a thumbnail
-
+
+
+
+
+
+
+ Does a file that is valid and as small as the format allows get through?
-
-
- •
-
-
-
-
-
-
- storage that reads nought bytes as a failed upload and keeps retrying
-
+
+
+
+
+
+
+
+ Typically finds:
-
-
- •
+
+
+
+
+
+ •
+
+
+
+
+ a valid file turned away for being too small, where the check counts bytes instead of reading them
+
+
+
+
+
+
+ •
+
+
+
+
+ an empty file that brings the reader down rather than being reported
+
+
+
+
+
+
+ •
+
+
+
+
+ a picture one pixel wide that divides by zero on the way to a thumbnail
+
+
+
+
+
+
+ •
+
+
+
+
+ storage that reads nought bytes as a failed upload and keeps retrying
+
+
+
+
+
-
-
-
-
- Settings
-
-
-
-
- Formats
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+
-
-
-
-
-
-
-
-
- all
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Formats
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: all
+
+
+
+
-
-
+
+
@@ -202,74 +252,101 @@
-
-
-
-
- Output
-
-
-
- Output directory
-
- *
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
-
-
+
+
+
+
-
-
-
-
- Choose...
-
+ Output
+
+
+
+
+
-
+
+
-
+
-
- Seed
-
- *
-
-
-
-
-
-
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
+
+
+
+
+
+ Choose...
+
+
+
+
+
-
-
-
-
-
-
-
-
- 0
+
+
+
+
+ Seed
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
-
-
+
+
@@ -288,28 +365,25 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
-
-
-
-
- 28 files · 31.9 KB (32 667 B) · avif, bmp, csv, docx, gif, html, ico, jpg, json, jxl, log, md, pdf, png, pptx, svg, targz, tiff,
- toml, txt, wav, webp, xlsx, xml, yaml, zip · will go to /tfg/out
+
+
+
+
+ 28 files · 31.9 KB (32 667 B) · 26 formats · will go to /tfg/out
@@ -321,11 +395,11 @@
-
-
-
-
- Donate
+
+
+
+
+ Donate
diff --git a/internal/guard/testdata/screens/recipe-contents.png b/internal/guard/testdata/screens/recipe-contents.png
index f02f016..0e6370b 100644
Binary files a/internal/guard/testdata/screens/recipe-contents.png and b/internal/guard/testdata/screens/recipe-contents.png differ
diff --git a/internal/guard/testdata/screens/recipe-contents.xml b/internal/guard/testdata/screens/recipe-contents.xml
index 2bb4cd5..b76371e 100644
--- a/internal/guard/testdata/screens/recipe-contents.xml
+++ b/internal/guard/testdata/screens/recipe-contents.xml
@@ -32,8 +32,8 @@
-
-
+
+
@@ -56,49 +56,79 @@
-
-
-
-
-
- Build on a preset
-
-
-
-
- With the switch on, the chosen preset's files come first and the batches below are added after them. With it off, the batches are the whole
- recipe.
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+ Build on a preset
+
+
+
+
+
+
+
+
+
-
-
-
-
- Start from a preset
-
-
-
-
+
+
+
+
+
+
+ With the box ticked, the chosen preset's files come first and the batches below are added after them. Without it, the batches are the whole
+ recipe.
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Start from a preset
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -108,10 +138,10 @@
- Batch 1
-
+
+ Batch 1
@@ -120,18 +150,18 @@
-
-
-
- Duplicate
+
+
+
+ Duplicate
-
-
-
-
+
+
+
+
Format
@@ -139,81 +169,52 @@
-
-
-
-
+
+
+
+
-
+
zip
-
-
+
+
-
+
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ How many files
-
-
-
-
-
-
-
-
- files
-
-
-
-
-
-
-
-
-
-
-
- How many files
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+
+
+
+
+ default: 1
-
+
-
+
-
-
-
+
+
+
How the size is given
@@ -221,7 +222,7 @@
-
+
@@ -233,50 +234,81 @@
-
-
-
- Size
-
- *
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+ 10 485 760 B
+
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
-
-
+
+
-
-
-
-
- 10 485 760 B
-
-
-
-
-
-
-
+
+
+
File names
@@ -284,80 +316,100 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
files_0001
-
+
-
+
-
-
-
-
-
-
-
-
-
-
- Settings for zip
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for zip
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
-
+
+
+
-
-
-
+
@@ -367,65 +419,71 @@
-
- Format
- How many files
- Size
-
+
+
+ Format
+
+
+ How many files
+
+
+ Size
+
+
-
+
-
-
-
+
+
+
-
+
(Select one)
-
-
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
+
@@ -441,7 +499,7 @@
-
+
Add files inside
@@ -453,126 +511,158 @@
-
-
-
-
-
- Output
-
-
-
- Output directory
-
- *
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- /tfg/out
-
-
+
+
+
+
-
-
-
-
- Choose...
-
+ Output
+
+
+
+
+
-
+
+
-
+
-
- Manifest file name
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- manifest.json
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
+
+
+
+
+ Choose...
+
+
+
+
+
+
+
+
+
+
+ Manifest file name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ manifest.json
+
+
+
+
-
-
+
+
-
-
-
-
- Seed
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
-
-
-
+
+
+
+ Seed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: 0
+
+
+
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -581,14 +671,6 @@
-
-
-
-
-
-
-
-
@@ -598,18 +680,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -631,17 +711,17 @@
-
-
-
- Donate
+
+
+
+ Donate
-
+
-
-
-
- Add a batch
+
+
+
+ Add a batch
diff --git a/internal/guard/testdata/screens/recipe-on-a-preset.png b/internal/guard/testdata/screens/recipe-on-a-preset.png
index 1287862..daf21b5 100644
Binary files a/internal/guard/testdata/screens/recipe-on-a-preset.png and b/internal/guard/testdata/screens/recipe-on-a-preset.png differ
diff --git a/internal/guard/testdata/screens/recipe-on-a-preset.xml b/internal/guard/testdata/screens/recipe-on-a-preset.xml
index d281ae4..cf853bd 100644
--- a/internal/guard/testdata/screens/recipe-on-a-preset.xml
+++ b/internal/guard/testdata/screens/recipe-on-a-preset.xml
@@ -32,8 +32,8 @@
-
-
+
+
@@ -56,94 +56,124 @@
-
-
-
-
-
- Build on a preset
-
-
-
-
- With the switch on, the chosen preset's files come first and the batches below are added after them. With it off, the batches are the whole
- recipe.
-
-
-
-
-
-
-
- Start from a preset
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Build on a preset
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- Preset to build on
-
-
-
-
+
+
+
+
+
+ With the box ticked, the chosen preset's files come first and the batches below are added after them. Without it, the batches are the whole
+ recipe.
+
-
-
-
-
-
-
- empty-and-minimal
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ Start from a preset
+
+
+
+
+
+
+
-
-
-
-
-
- Formats
-
-
-
-
-
+
+
+
+ Preset to build on
+
+
+
+
+
+
+
+
+
+
+
+
+ empty-and-minimal
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- all
-
-
-
+
+
+
+
+ Formats
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: all
+
+
+
+
-
-
+
+
@@ -152,25 +182,25 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
- Batch 1
-
+
+
+
+ Batch 1
@@ -178,24 +208,24 @@
-
-
-
-
- Duplicate
+
+
+
+
+ Duplicate
-
-
-
- Remove
+
+
+
+ Remove
-
-
-
-
+
+
+
+
Format
@@ -203,81 +233,52 @@
-
-
-
-
+
+
+
+
-
+
avif
-
-
+
+
-
+
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ How many files
-
-
-
-
-
-
-
-
- files
+
+
+
+
+
+
+
+
+ default: 1
-
-
-
-
-
-
-
-
-
-
- How many files
-
-
-
-
-
-
-
-
- 1
-
-
+
-
+
-
-
-
+
+
+
How the size is given
@@ -285,7 +286,7 @@
-
+
@@ -297,50 +298,81 @@
-
-
-
- Size
-
- *
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+ 10 485 760 B
+
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
-
-
+
+
-
-
-
-
- 10 485 760 B
-
-
-
-
-
-
-
+
+
+
File names
@@ -348,76 +380,96 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
files_0001
-
+
-
+
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
-
+
+
+
-
-
@@ -427,126 +479,158 @@
-
-
-
-
-
- Output
-
-
-
- Output directory
-
- *
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- /tfg/out
-
-
+
+
+
+
-
-
-
-
- Choose...
-
+ Output
+
+
+
+
+
-
+
+
-
+
-
- Manifest file name
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- manifest.json
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
+
+
+
+
+ Choose...
+
+
+
+
+
+
+
+
+
+
+ Manifest file name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ manifest.json
+
+
+
+
-
-
+
+
-
-
-
-
- Seed
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
-
-
-
+
+
+
+ Seed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: 0
+
+
+
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -555,14 +639,6 @@
-
-
-
-
-
-
-
-
@@ -572,28 +648,25 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
-
-
-
-
- 29 files · 10.0 MB (10 518 427 B) · avif, bmp, csv, docx, gif, html, ico, jpg, json, jxl, log, md, pdf, png, pptx, svg, targz,
- tiff, toml, txt, wav, webp, xlsx, xml, yaml, zip · will go to /tfg/out
+
+
+
+
+ 29 files · 10.0 MB (10 518 427 B) · 26 formats · will go to /tfg/out
@@ -606,17 +679,17 @@
-
-
-
- Donate
+
+
+
+ Donate
-
+
-
-
-
- Add a batch
+
+
+
+ Add a batch
diff --git a/internal/guard/testdata/screens/recipe-refused-with-one-batch-filled.png b/internal/guard/testdata/screens/recipe-refused-with-one-batch-filled.png
index 071e40c..16674ca 100644
Binary files a/internal/guard/testdata/screens/recipe-refused-with-one-batch-filled.png and b/internal/guard/testdata/screens/recipe-refused-with-one-batch-filled.png differ
diff --git a/internal/guard/testdata/screens/recipe-refused-with-one-batch-filled.xml b/internal/guard/testdata/screens/recipe-refused-with-one-batch-filled.xml
index 031720d..6120a32 100644
--- a/internal/guard/testdata/screens/recipe-refused-with-one-batch-filled.xml
+++ b/internal/guard/testdata/screens/recipe-refused-with-one-batch-filled.xml
@@ -32,8 +32,8 @@
-
-
+
+
@@ -56,62 +56,92 @@
-
-
-
-
-
- Build on a preset
-
-
-
-
- With the switch on, the chosen preset's files come first and the batches below are added after them. With it off, the batches are the whole
- recipe.
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+ Build on a preset
+
+
+
+
+
+
+
+
+
-
-
-
-
- Start from a preset
-
-
-
-
+
+
+
+
+
+
+ With the box ticked, the chosen preset's files come first and the batches below are added after them. Without it, the batches are the whole
+ recipe.
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Start from a preset
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
- Batch 1
-
+
+
+
+ Batch 1
@@ -119,24 +149,24 @@
-
-
-
-
- Duplicate
+
+
+
+
+ Duplicate
-
-
-
- Remove
+
+
+
+ Remove
-
-
-
-
+
+
+
+
Format
@@ -144,81 +174,52 @@
-
-
-
-
+
+
+
+
-
+
avif
-
-
+
+
-
+
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ How many files
-
-
-
-
-
-
-
-
- files
+
+
+
+
+
+
+
+
+ default: 1
-
-
-
-
-
-
-
-
-
-
- How many files
-
-
-
-
-
-
-
-
- 1
-
-
+
-
+
-
-
-
+
+
+
How the size is given
@@ -226,7 +227,7 @@
-
+
@@ -238,50 +239,114 @@
-
-
-
- Size
-
- *
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- 10mb
-
-
-
+
+
+
+
+
+
+
+ target 1 has no Size - every target declares its Size, which is what lets a dry run report exact numbers before anything
+ reaches the disk.
+ add size: 2mb, size-range: 1kb-8kb, a boundary, contains, or a plain number of bytes.
-
-
+
-
-
-
-
- 10 485 760 B
+
+
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+ target 1 has no Batch name - a Batch name anchors the seed of a target, so editing one
+ target never moves the bytes of another.
+ give it a Batch name, for example id: invoices.
+
+
+
+
+
-
-
-
+
+
+
File names
@@ -289,76 +354,96 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
files_0001
-
+
-
+
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
-
+
+
+
-
-
@@ -366,23 +451,23 @@
-
-
-
-
+
+
+
+
-
-
+
+
-
-
- Batch 2
-
+
+
+
+ Batch 2
@@ -390,24 +475,24 @@
-
-
-
-
- Duplicate
+
+
+
+
+ Duplicate
-
-
-
- Remove
+
+
+
+ Remove
-
-
-
-
+
+
+
+
Format
@@ -415,81 +500,52 @@
-
-
-
-
+
+
+
+
-
+
avif
-
-
+
+
-
+
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- second
-
-
-
-
-
-
+
+
+
+ How many files
-
-
-
-
- How many files
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+
+
+
+
+ default: 1
-
+
-
+
-
-
-
+
+
+
How the size is given
@@ -497,7 +553,7 @@
-
+
@@ -509,50 +565,81 @@
-
-
-
- Size
-
- *
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1kb
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+ 1 024 B
+
-
-
-
-
-
-
-
-
- 1kb
-
+
+
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ second
-
-
+
+
-
-
-
-
- 1 024 B
-
-
-
-
-
-
-
+
+
+
File names
@@ -560,76 +647,96 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
files_0001
-
+
-
+
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
-
+
+
+
-
-
@@ -639,126 +746,158 @@
-
-
-
-
-
- Output
-
-
-
- Output directory
-
- *
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
-
-
+
+
+
+
-
-
-
-
- Choose...
-
+ Output
+
+
+
+
+
-
+
+
-
+
-
- Manifest file name
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- manifest.json
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
+
+
+
+
+ Choose...
+
+
+
+
+
+
+
+
+
+
+ Manifest file name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ manifest.json
+
+
+
+
-
-
+
+
-
-
-
-
- Seed
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
-
-
-
+
+
+
+ Seed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: 0
+
+
+
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -771,8 +910,8 @@
-
-
+
+
@@ -784,18 +923,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -804,7 +941,7 @@
- 2 files · 10.0 MB (10 486 784 B) · avif · will go to /tfg/out
+ Files will go to /tfg/out
@@ -817,17 +954,17 @@
-
-
-
- Donate
+
+
+
+ Donate
-
+
-
-
-
- Add a batch
+
+
+
+ Add a batch
diff --git a/internal/guard/testdata/screens/recipe-refused.png b/internal/guard/testdata/screens/recipe-refused.png
index 3666a4c..e37fe56 100644
Binary files a/internal/guard/testdata/screens/recipe-refused.png and b/internal/guard/testdata/screens/recipe-refused.png differ
diff --git a/internal/guard/testdata/screens/recipe-refused.xml b/internal/guard/testdata/screens/recipe-refused.xml
index 7c7c6b6..53654ec 100644
--- a/internal/guard/testdata/screens/recipe-refused.xml
+++ b/internal/guard/testdata/screens/recipe-refused.xml
@@ -32,8 +32,8 @@
-
-
+
+
@@ -56,49 +56,79 @@
-
-
-
-
-
- Build on a preset
-
-
-
-
- With the switch on, the chosen preset's files come first and the batches below are added after them. With it off, the batches are the whole
- recipe.
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+ Build on a preset
+
+
+
+
+
+
+
+
+
-
-
-
-
- Start from a preset
-
-
-
-
+
+
+
+
+
+
+ With the box ticked, the chosen preset's files come first and the batches below are added after them. Without it, the batches are the whole
+ recipe.
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Start from a preset
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -108,10 +138,10 @@
- Batch 1
-
+
+ Batch 1
@@ -120,18 +150,18 @@
-
-
-
- Duplicate
+
+
+
+ Duplicate
-
-
-
-
+
+
+
+
Format
@@ -139,81 +169,52 @@
-
-
-
-
+
+
+
+
-
+
avif
-
-
+
+
-
+
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ How many files
-
-
-
-
-
-
-
-
- files
-
-
-
-
-
-
-
-
-
-
-
- How many files
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+
+
+
+
+ default: 1
-
+
-
+
-
-
-
+
+
+
How the size is given
@@ -221,7 +222,7 @@
-
+
@@ -233,50 +234,114 @@
-
-
-
- Size
-
- *
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- 10mb
-
-
-
+
+
+
+
+
+
+
+ target 1 has no Size - every target declares its Size, which is what lets a dry run report exact numbers before anything
+ reaches the disk.
+ add size: 2mb, size-range: 1kb-8kb, a boundary, contains, or a plain number of bytes.
-
-
+
-
-
-
-
- 10 485 760 B
+
+
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+ target 1 has no Batch name - a Batch name anchors the seed of a target, so editing one
+ target never moves the bytes of another.
+ give it a Batch name, for example id: invoices.
+
+
+
+
+
-
-
-
+
+
+
File names
@@ -284,76 +349,96 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
files_0001
-
+
-
+
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
-
+
+
+
-
-
@@ -363,126 +448,158 @@
-
-
-
-
-
- Output
-
-
-
- Output directory
-
- *
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- /tfg/out
-
-
+
+
+
+
-
-
-
-
- Choose...
-
+ Output
+
+
+
+
+
-
+
+
-
+
-
- Manifest file name
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- manifest.json
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
+
+
+
+
+ Choose...
+
+
+
+
+
+
+
+
+
+
+ Manifest file name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ manifest.json
+
+
+
+
-
-
+
+
-
-
-
-
- Seed
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
-
-
-
+
+
+
+ Seed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: 0
+
+
+
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -491,14 +608,6 @@
-
-
-
-
-
-
-
-
@@ -508,18 +617,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -528,7 +635,7 @@
- 1 file · 10.0 MB (10 485 760 B) · avif · will go to /tfg/out
+ Files will go to /tfg/out
@@ -541,17 +648,17 @@
-
-
-
- Donate
+
+
+
+ Donate
-
+
-
-
-
- Add a batch
+
+
+
+ Add a batch
diff --git a/internal/guard/testdata/screens/recipe-two-batches.png b/internal/guard/testdata/screens/recipe-two-batches.png
index 6cba132..15cde6f 100644
Binary files a/internal/guard/testdata/screens/recipe-two-batches.png and b/internal/guard/testdata/screens/recipe-two-batches.png differ
diff --git a/internal/guard/testdata/screens/recipe-two-batches.xml b/internal/guard/testdata/screens/recipe-two-batches.xml
index 9908862..4e2d988 100644
--- a/internal/guard/testdata/screens/recipe-two-batches.xml
+++ b/internal/guard/testdata/screens/recipe-two-batches.xml
@@ -32,8 +32,8 @@
-
-
+
+
@@ -56,62 +56,92 @@
-
-
-
-
-
- Build on a preset
-
-
-
-
- With the switch on, the chosen preset's files come first and the batches below are added after them. With it off, the batches are the whole
- recipe.
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+ Build on a preset
+
+
+
+
+
+
+
+
+
-
-
-
-
- Start from a preset
-
-
-
-
+
+
+
+
+
+
+ With the box ticked, the chosen preset's files come first and the batches below are added after them. Without it, the batches are the whole
+ recipe.
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Start from a preset
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
- Batch 1
-
+
+
+
+ Batch 1
@@ -119,24 +149,24 @@
-
-
-
-
- Duplicate
+
+
+
+
+ Duplicate
-
-
-
- Remove
+
+
+
+ Remove
-
-
-
-
+
+
+
+
Format
@@ -144,81 +174,52 @@
-
-
-
-
+
+
+
+
-
+
avif
-
-
+
+
-
+
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ How many files
-
-
-
-
-
-
-
-
- files
+
+
+
+
+
+
+
+
+ default: 1
-
-
-
-
-
-
-
-
-
-
- How many files
-
-
-
-
-
-
-
-
- 1
-
-
+
-
+
-
-
-
+
+
+
How the size is given
@@ -226,7 +227,7 @@
-
+
@@ -238,50 +239,81 @@
-
-
-
- Size
-
- *
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+ 10 485 760 B
+
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
-
-
+
+
-
-
-
-
- 10 485 760 B
-
-
-
-
-
-
-
+
+
+
File names
@@ -289,76 +321,96 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
files_0001
-
+
-
+
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
-
+
+
+
-
-
@@ -366,23 +418,23 @@
-
-
-
-
+
+
+
+
-
-
+
+
-
-
- Batch 2
-
+
+
+
+ Batch 2
@@ -390,24 +442,24 @@
-
-
-
-
- Duplicate
+
+
+
+
+ Duplicate
-
-
-
- Remove
+
+
+
+ Remove
-
-
-
-
+
+
+
+
Format
@@ -415,84 +467,52 @@
-
-
-
-
+
+
+
+
-
+
avif
-
-
+
+
-
+
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
+
+
+
+ How many files
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ default: 1
-
+
-
+
-
-
- How many files
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
How the size is given
@@ -500,7 +520,7 @@
-
+
@@ -512,53 +532,87 @@
-
-
-
- Size
-
- *
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
File names
@@ -566,76 +620,96 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
files_0001
-
+
-
+
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
-
+
+
+
-
-
@@ -645,126 +719,158 @@
-
-
-
-
-
- Output
-
-
-
- Output directory
-
- *
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- /tfg/out
-
-
+
+
+
+
-
-
-
-
- Choose...
-
+ Output
+
+
+
+
+
-
+
+
-
+
-
- Manifest file name
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- manifest.json
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
+
+
+
+
+ Choose...
+
+
+
+
+
+
+
+
+
+
+ Manifest file name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ manifest.json
+
+
+
+
-
-
+
+
-
-
-
-
- Seed
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
-
-
-
+
+
+
+ Seed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: 0
+
+
+
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -777,8 +883,8 @@
-
-
+
+
@@ -790,18 +896,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -823,17 +927,17 @@
-
-
-
- Donate
+
+
+
+ Donate
-
+
-
-
-
- Add a batch
+
+
+
+ Add a batch
diff --git a/internal/guard/testdata/screens/recipe.png b/internal/guard/testdata/screens/recipe.png
index 3666a4c..b30c21d 100644
Binary files a/internal/guard/testdata/screens/recipe.png and b/internal/guard/testdata/screens/recipe.png differ
diff --git a/internal/guard/testdata/screens/recipe.xml b/internal/guard/testdata/screens/recipe.xml
index 87d4895..3532cfd 100644
--- a/internal/guard/testdata/screens/recipe.xml
+++ b/internal/guard/testdata/screens/recipe.xml
@@ -32,8 +32,8 @@
-
-
+
+
@@ -56,49 +56,79 @@
-
-
-
-
-
- Build on a preset
-
-
-
-
- With the switch on, the chosen preset's files come first and the batches below are added after them. With it off, the batches are the whole
- recipe.
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+ Build on a preset
+
+
+
+
+
+
+
+
+
-
-
-
-
- Start from a preset
-
-
-
-
+
+
+
+
+
+
+ With the box ticked, the chosen preset's files come first and the batches below are added after them. Without it, the batches are the whole
+ recipe.
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Start from a preset
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -108,10 +138,10 @@
- Batch 1
-
+
+ Batch 1
@@ -120,18 +150,18 @@
-
-
-
- Duplicate
+
+
+
+ Duplicate
-
-
-
-
+
+
+
+
Format
@@ -139,81 +169,52 @@
-
-
-
-
+
+
+
+
-
+
avif
-
-
+
+
-
+
-
-
-
- Batch name
-
- *
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- files
-
-
-
-
-
-
+
+
+
+ How many files
-
-
-
-
- How many files
-
-
-
-
-
-
-
-
- 1
+
+
+
+
+
+
+
+
+ default: 1
-
+
-
+
-
-
-
+
+
+
How the size is given
@@ -221,7 +222,7 @@
-
+
@@ -233,50 +234,81 @@
-
-
-
- Size
-
- *
+
+
+
+
+ Size
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10mb
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+ 10 485 760 B
+
-
-
-
-
-
-
-
-
- 10mb
-
+
+
+
+
+
+
+ Batch name
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ files
-
-
+
+
-
-
-
-
- 10 485 760 B
-
-
-
-
-
-
-
+
+
+
File names
@@ -284,76 +316,96 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
files_0001
-
+
-
+
-
-
-
-
-
-
-
-
-
-
- Settings for avif
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings for avif
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Notes for the manifest
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Notes for the manifest
+
+
+
+
+
-
+
+
+
-
-
@@ -363,126 +415,158 @@
-
-
-
-
-
- Output
-
-
-
- Output directory
-
- *
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /tfg/out
-
-
+
+
+
+
-
-
-
-
- Choose...
-
+ Output
+
+
+
+
+
-
+
+
-
+
-
- Manifest file name
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- manifest.json
+
+
+
+ Output directory
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /tfg/out
+
+
-
-
+
+
+
+
+ Choose...
+
+
+
+
+
+
+
+
+
+
+ Manifest file name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ manifest.json
+
+
+
+
-
-
+
+
-
-
-
-
- Seed
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
-
-
-
+
+
+
+ Seed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default: 0
+
+
+
+
-
-
+
+
-
-
-
-
- Label in each file
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Label in each file
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
@@ -491,14 +575,6 @@
-
-
-
-
-
-
-
-
@@ -508,18 +584,16 @@
-
-
-
-
- Preview
+
+
+
+ Preview
-
-
-
- Generate
+
+
+
+ Generate
-
@@ -541,17 +615,17 @@
-
-
-
- Donate
+
+
+
+ Donate
-
+
-
-
-
- Add a batch
+
+
+
+ Add a batch
diff --git a/internal/guard/window_test.go b/internal/guard/window_test.go
index 2918360..96e50a5 100644
--- a/internal/guard/window_test.go
+++ b/internal/guard/window_test.go
@@ -430,9 +430,14 @@ func TestTheAboutScreenSaysWhoOwnsTheGeneratedFiles(t *testing.T) {
// each other, which is exactly the shape in which a second copy gets written
// and nobody compares the two again.
func TestTheWindowAndTheCommandQuoteTheSameLicence(t *testing.T) {
- shown := textIn(window.About(newFakeHost(t)))
- if !strings.Contains(shown, strings.TrimSpace(version.LicenceNotice)) {
- t.Error("the window does not show the licence notice verbatim, so it is a second copy now")
+ // Word for word rather than line for line since 2026-09-23. The notice
+ // is broken into lines for a terminal, and the window joins the lines of
+ // each paragraph so it wraps to its own width instead of drawing a
+ // narrow ragged column - so the breaks differ and the words must not.
+ shown := strings.Join(strings.Fields(textIn(window.About(newFakeHost(t)))), " ")
+ notice := strings.Join(strings.Fields(version.LicenceNotice), " ")
+ if !strings.Contains(shown, notice) {
+ t.Error("the window does not show the words of the licence notice, so it is a second copy now")
}
}
@@ -707,6 +712,19 @@ func controlUnder(o fyne.CanvasObject, label string) fyne.CanvasObject {
if isHeadingExtra(box.Objects[1]) {
return
}
+ // The name beside a box to tick is its words with a NameTap over
+ // them, which is a label followed by something - see parts.NameTap.
+ if _, tap := box.Objects[1].(*parts.NameTap); tap {
+ return
+ }
+ // A box to tick carries its name BESIDE the square since 2026-09-23 -
+ // parts.ToggleSaying - so its line is the square and then the name.
+ if check, isToggle := box.Objects[0].(*parts.Toggle); isToggle {
+ if head, named := headingOf(box.Objects[1]); named && head == label {
+ found = check
+ }
+ return
+ }
if head, named := headingOf(box.Objects[0]); named && head == label {
found = unringed(box.Objects[1])
}
diff --git a/internal/gui/catalogue/catalogue.go b/internal/gui/catalogue/catalogue.go
index 6af4461..aa166c3 100644
--- a/internal/gui/catalogue/catalogue.go
+++ b/internal/gui/catalogue/catalogue.go
@@ -98,6 +98,8 @@ func NotDrawn() []Reason {
{"PointerFocus", "a piece inside a control, knowing what put the keyboard there - no picture of its own"},
{"Returnable", "an interface: a control the window can tell it is coming back to the front, nothing of its own to draw"},
{"Shortcuts", "a keyboard map: nothing to draw, and saying so is the point of this row"},
+ {"GroupKind", "an enum: what a group of settings is about, every value drawn under Folding as the colour of its rail"},
+ {"NameTap", "an invisible target over the name beside a box to tick, so pressing the name ticks the box - nothing of its own to draw"},
}
}
@@ -120,6 +122,9 @@ func LayoutOnly() []Reason {
{"WithRoomForARun", "the room under a form for the bar to speak into"},
{"Flush", "a label kept by a screen, on the edge every other word stands on"},
{"Clear", "a shape that takes room and draws nothing"},
+ {"Grid", "lays a section's fields in columns, each taking the columns its value needs - drawn under Section"},
+ {"Wide", "marks something that takes a whole row of a Grid"},
+ {"ButtonRow", "buttons side by side, one gap of the scale apart and centred - drawn under Section"},
}
}
diff --git a/internal/gui/catalogue/fields.go b/internal/gui/catalogue/fields.go
index d4980c8..b12bf67 100644
--- a/internal/gui/catalogue/fields.go
+++ b/internal/gui/catalogue/fields.go
@@ -21,7 +21,7 @@ func fields() Entry {
e.SetText("10mb")
return s.Add("size", "Size", "10mb", parts.NoDetail, parts.Numeric(e))
}
- return Entry{Name: "Fields", Covers: []string{"FieldSaying", "CellSaying", "FieldStack", "RequiredMark", "Table"}, States: []State{
+ return Entry{Name: "Fields", Covers: []string{"FieldSaying", "CellSaying", "FieldStack", "RequiredMark", "Table", "ToggleSaying"}, States: []State{
{"a row", func() fyne.CanvasObject { return sizeRow(form()) }},
{"a row that has to be filled in", func() fyne.CanvasObject {
s := form()
@@ -40,10 +40,16 @@ func fields() Entry {
s.Freeze(true)
return row
}},
- {"a switch under its name", func() fyne.CanvasObject {
+ {"a box to tick with its name beside it", func() fyne.CanvasObject {
s := form()
return s.AddToggle("label", "Label in each file", "", parts.NoDetail, parts.NewToggle(func(bool) {}))
}},
+ {"a box to tick beside a named field, level with its box", func() fyne.CanvasObject {
+ s := form()
+ return parts.Grid(
+ s.Add("seed", "Seed", "0", parts.NoDetail, parts.Numeric(parts.NewEntry())),
+ s.AddToggle("label", "Label in each file", "", parts.NoDetail, parts.NewToggle(func(bool) {})))
+ }},
{"a named control with no setting behind it", func() fyne.CanvasObject {
// It looks like a field and is not one: nothing is ever refused
// about it, so it carries no room for a message and no star. The
@@ -175,6 +181,12 @@ func errorArea() Entry {
a.Say(longText + ". " + longText)
return a.Object()
}},
+ {"offering the fix it asks for", func() fyne.CanvasObject {
+ a := parts.NewErrorArea()
+ a.Say("AVIF cannot be smaller than 311 B. Requested: 1 B")
+ a.Offer("Use the smallest size, 311 B", func() {})
+ return a.Object()
+ }},
}}
}
@@ -200,7 +212,7 @@ func folding() Entry {
// keyboard states a control has - and FoldHead is covered here rather
// than as an entry of its own, because it is never on a screen without
// the fold it heads.
- return Entry{Name: "Folding", Covers: []string{"InnerFolding", "FoldHead"}, States: []State{
+ return Entry{Name: "Folding", Covers: []string{"InnerFolding", "InnerFoldingOf", "FoldHead"}, States: []State{
{"open", func() fyne.CanvasObject {
return parts.NewFolding("Notes for the manifest", nil, parts.Prose("inside the fold")).Object()
}},
@@ -223,6 +235,16 @@ func folding() Entry {
f.Set(false)
return f.Object()
}},
+ // The rail down a group's left edge is the colour of what the group
+ // is about, so all three kinds are drawn.
+ {"inner, a damage's settings", func() fyne.CanvasObject {
+ return parts.NewInnerFoldingOf(parts.GroupDamage, "Settings for zero-head",
+ parts.Prose("inside the inner fold")).Object()
+ }},
+ {"inner, notes for the manifest", func() fyne.CanvasObject {
+ return parts.NewInnerFoldingOf(parts.GroupNotes, "Notes for the manifest",
+ parts.Prose("inside the inner fold")).Object()
+ }},
{"a long title", func() fyne.CanvasObject {
return parts.NewFolding(longText, nil, parts.Prose("inside the fold")).Object()
}},
diff --git a/internal/gui/catalogue/words.go b/internal/gui/catalogue/words.go
index 5b6e7da..9eb2168 100644
--- a/internal/gui/catalogue/words.go
+++ b/internal/gui/catalogue/words.go
@@ -1,9 +1,10 @@
package catalogue
import (
+ "errors"
+
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
- "fyne.io/fyne/v2/layout"
"github.com/donislawdev/TestingFilesGenerator/internal/gui/parts"
)
@@ -12,7 +13,7 @@ import (
// thing a person looks at as a whole, which is why every rank is here once,
// one under another, and once more with a long line to see where it wraps.
func textRanks() Entry {
- return Entry{Name: "Title", Covers: []string{"Subtitle", "Titled", "Heading", "Subheading", "Prose", "Note", "Caption", "Bullets"}, States: []State{
+ return Entry{Name: "Title", Covers: []string{"Subtitle", "Titled", "Heading", "Subheading", "Prose", "Note", "Caption", "Bullets", "Ledger"}, States: []State{
{"the title of a screen", func() fyne.CanvasObject { return parts.Title("Single batch") }},
{"the sentence under a title", func() fyne.CanvasObject {
return parts.Subtitle("Files of one format and one size, with a manifest that says how the system under test should react to them.")
@@ -47,6 +48,18 @@ func textRanks() Entry {
{"a caption, the smallest rank", func() fyne.CanvasObject {
return parts.Caption("10 485 760 B")
}},
+ // A table of words - what is carried in the binary, under which
+ // licence, and whose - and the row whose first two columns ask for
+ // so much that the last would be left a sliver, which is the one the
+ // About screen really has.
+ {"a table of three columns, the last one wrapping", func() fyne.CanvasObject {
+ return parts.Ledger([][3]string{
+ {"fyne.io/fyne/v2", "BSD-3-Clause", "(C) 2018 Fyne.io developers (see AUTHORS)"},
+ {"Mesa 3D, llvmpipe software renderer 26.2.0", "MIT AND Apache-2.0 WITH LLVM-exception AND BSL-1.0",
+ "Copyright (C) 1999-2007 Brian Paul, Copyright (C) 2008 VMware, Inc."},
+ {"golang.org/x/text", "BSD-3-Clause", longText},
+ })
+ }},
{"a long line at every rank", func() fyne.CanvasObject {
return parts.Column(parts.GapField,
parts.Title(longText), parts.Heading(longText), parts.Subheading(longText),
@@ -74,12 +87,30 @@ func structure() Entry {
}},
{"the bar a run starts from", func() fyne.CanvasObject {
// Composed the way the work screens compose it: the rail at the
- // left edge, the buttons centred between two spacers.
- return parts.ActionBar(container.NewHBox(parts.NewButton(parts.Quiet, "Donate", func() {})),
- container.NewHBox(layout.NewSpacer(),
- parts.NewButton(parts.Secondary, "Preview", func() {}),
- parts.NewButton(parts.Primary, "Generate", func() {}),
- layout.NewSpacer()))
+ // left edge, the buttons in a row of their own, centred.
+ return parts.ActionBar(container.NewHBox(parts.NewButton(parts.Quiet, "Donate", func() {}).InTheBar()),
+ parts.ButtonRow(
+ parts.NewButton(parts.Secondary, "Preview", func() {}).InTheBar(),
+ parts.NewButton(parts.Primary, "Generate", func() {}).InTheBar()))
+ }},
+ {"fields in columns, each as wide as its value needs", func() fyne.CanvasObject {
+ s := form()
+ return parts.Section("File configuration",
+ s.Add("format", "Format", "", parts.NoDetail, parts.NewChooser([]string{"avif", "png"}, nil)),
+ s.Add("size", "Size", "10mb", parts.NoDetail, parts.Numeric(parts.NewEntry())),
+ s.Add("count", "How many files", "1", parts.NoDetail, parts.Numeric(parts.NewEntry())),
+ s.Add("damage", "Damage", "", parts.NoDetail, parts.NewChooser([]string{"none"}, nil)),
+ parts.Wide(s.Add("dir", "Output directory", "", parts.NoDetail, parts.NewEntry())))
+ }},
+ {"a row with a refusal under it", func() fyne.CanvasObject {
+ s := form()
+ grid := parts.Section("File configuration",
+ s.Add("size", "Size", "10mb", parts.NoDetail, parts.Numeric(parts.NewEntry())),
+ s.Add("count", "How many files", "1", parts.NoDetail, parts.Numeric(parts.NewEntry())),
+ s.Add("seed", "Seed", "0", parts.NoDetail, parts.Numeric(parts.NewEntry())))
+ s.Mark("size", errors.New("size 3 B is below the smallest png, which is 73 B"))
+ s.Mark("count", errors.New(longText))
+ return grid
}},
{"a section with a long title", func() fyne.CanvasObject {
return parts.Section(longText, twoRows()...)
diff --git a/internal/gui/parts/button.go b/internal/gui/parts/button.go
index eec296a..1424d98 100644
--- a/internal/gui/parts/button.go
+++ b/internal/gui/parts/button.go
@@ -70,6 +70,10 @@ type Button struct {
look Look
+ // inBar is whether this button stands in the bar at the foot, where it
+ // keeps more room round its words - see InTheBar.
+ inBar bool
+
// The pointer's state and the keyboard's, kept here because the toolkit
// keeps its own in unexported fields a renderer of ours cannot read.
//
@@ -101,6 +105,15 @@ func NewButton(look Look, label string, tapped func()) *Button {
return b
}
+// InTheBar gives a button the size of the bar at the foot of a screen, where
+// the buttons that run something stand. A parameter of the one button rather
+// than a second kind of button, so every look can stand there.
+func (b *Button) InTheBar() *Button {
+ b.inBar = true
+ b.Refresh()
+ return b
+}
+
// NewGlyphButton builds the small mark-only button an icon stands in.
func NewGlyphButton(icon fyne.Resource, tapped func()) *Button {
b := &Button{look: Glyph, Icon: icon, OnTapped: tapped}
@@ -291,6 +304,9 @@ func (r *buttonRenderer) MinSize() fyne.Size {
if r.button.Icon != nil {
size.Width += Theme().Size(theme.SizeNameInlineIcon) + GapInline
}
+ if r.button.inBar {
+ return size.Add(fyne.NewSize(BarButtonInsetX*2, BarButtonInsetY*2))
+ }
// The room inside a box to type in, on both axes, so a button stands the
// same height as the field beside it.
return size.Add(fyne.NewSquareSize(ControlInset * 2))
@@ -309,6 +325,12 @@ func (r *buttonRenderer) Refresh() {
r.label.Text = r.button.Text
r.label.Color = PaletteColour(f.ink, theme.VariantDark)
r.label.TextSize = TextBody
+ // A quiet button is words rather than a face, so it drops the weight and
+ // a rank of size - the prototype of 2026-09-23, see buttonFace.
+ r.label.TextStyle = fyne.TextStyle{Bold: r.button.look != Quiet}
+ if r.button.look == Quiet {
+ r.label.TextSize = TextCaption
+ }
if r.button.Icon != nil {
// Coloured by the same ink as the words, so a glyph follows the state
// of the button it stands in - the toolkit's own way of tinting a
@@ -428,7 +450,9 @@ func buttonFace(look Look, state buttonState) face {
return f
default: // Quiet and Glyph: no resting edge, a surface only under the pointer.
ink := theme.ColorNameForeground
- if look == Glyph && state == stateRest {
+ // Quiet as well since the prototype of 2026-09-23: Donate in bold
+ // white at rest read as a heading of the bar, as loud as Generate.
+ if (look == Glyph || look == Quiet) && state == stateRest {
ink = theme.ColorNamePlaceHolder
}
return face{fill: pointerFill(state), ink: ink}
diff --git a/internal/gui/parts/buttonrow.go b/internal/gui/parts/buttonrow.go
new file mode 100644
index 0000000..c9aa239
--- /dev/null
+++ b/internal/gui/parts/buttonrow.go
@@ -0,0 +1,52 @@
+package parts
+
+import (
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+)
+
+// ButtonRow is a row of buttons side by side, GapButtons apart and centred in
+// the room it is given: the ones that run something in the bar at the foot of
+// a screen, and the ones in the head of a batch.
+//
+// A layout of ours rather than a box, which is what both were until the
+// prototype of 2026-09-23: a box puts the toolkit's padding between its
+// children, about 4 px, and Preview and Generate - and Duplicate and Remove -
+// read as one control. What is hidden takes no room and no gap, so Cancel and
+// the two offers after a run come and go without leaving a hole.
+func ButtonRow(items ...fyne.CanvasObject) *fyne.Container {
+ return container.New(buttonRow{}, items...)
+}
+
+type buttonRow struct{}
+
+func (buttonRow) MinSize(objects []fyne.CanvasObject) fyne.Size {
+ size := fyne.NewSize(0, 0)
+ shown := 0
+ for _, o := range objects {
+ if !o.Visible() {
+ continue
+ }
+ min := o.MinSize()
+ size.Width += min.Width
+ size.Height = fyne.Max(size.Height, min.Height)
+ shown++
+ }
+ if shown > 1 {
+ size.Width += GapButtons * float32(shown-1)
+ }
+ return size
+}
+
+func (b buttonRow) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ x := (size.Width - b.MinSize(objects).Width) / 2
+ for _, o := range objects {
+ if !o.Visible() {
+ continue
+ }
+ min := o.MinSize()
+ o.Resize(min)
+ o.Move(fyne.NewPos(x, (size.Height-min.Height)/2))
+ x += min.Width + GapButtons
+ }
+}
diff --git a/internal/gui/parts/field.go b/internal/gui/parts/field.go
index 6b3979c..2f8309d 100644
--- a/internal/gui/parts/field.go
+++ b/internal/gui/parts/field.go
@@ -60,7 +60,7 @@ func FieldSaying(label string, detail Detail, required bool, trailing, control f
pieces = append(pieces, trailing)
}
body := FieldStack(pieces...)
- return Built{Object: Column(GapTight, body, area.Object()), Body: body, Area: area}
+ return Built{Object: cellOf(body, area.Object()), Body: body, Area: area}
}
// Built is what building a field comes to, in one hand: the whole thing to
@@ -149,6 +149,12 @@ type ErrorArea struct {
// edge is the line round the control, if it has one. Marked and cleared
// with the sentence, so the two never disagree.
edge *Ring
+ // fix is a button under the sentence that puts right what it is about,
+ // when the refusal carries the value that would - see Offer. fixRow is
+ // what is shown and hidden, so a field with nothing to offer keeps no
+ // room for a button.
+ fix *Button
+ fixRow fyne.CanvasObject
}
// NewErrorArea builds one that stands on its own, for the line at the foot of
@@ -160,7 +166,11 @@ func NewErrorArea() *ErrorArea {
// The sentence starts under the control it is about, on the edge the
// control starts on - which is the field's edge since the name stands
// over the control rather than beside it.
- area := &ErrorArea{label: label, box: container.NewVBox(inkTight(label))}
+ fix := NewButton(Secondary, "", nil)
+ fix.Hide()
+ fixRow := container.NewHBox(fix)
+ area := &ErrorArea{label: label, fix: fix, fixRow: fixRow,
+ box: Column(GapLabel, inkTight(label), fixRow)}
area.Clear()
return area
}
@@ -175,13 +185,42 @@ func (a *ErrorArea) Say(text string) {
return
}
a.label.SetText(text)
+ a.hideFix()
a.box.Show()
a.mark(true)
}
+// Offer puts a button under the sentence that does what the sentence asks
+// for, so nobody has to copy a number out of a refusal into the box above it.
+// Called after Say, and taken away by the next Say or Clear.
+func (a *ErrorArea) Offer(label string, apply func()) {
+ a.fix.SetText(label)
+ a.fix.OnTapped = apply
+ a.fix.Show()
+ a.fixRow.Show()
+}
+
+// hideFix takes the button away with its row. The button itself as well,
+// because a button still marked visible inside a hidden row is a button a
+// guard walking the tree counts as there to press, at 0x0.
+func (a *ErrorArea) hideFix() {
+ a.fix.Hide()
+ a.fixRow.Hide()
+}
+
+// Offered is the words on the button under the sentence, or nothing, for a
+// guard.
+func (a *ErrorArea) Offered() string {
+ if !a.fixRow.Visible() {
+ return ""
+ }
+ return a.fix.Text
+}
+
// Clear takes the sentence away and gives the room back.
func (a *ErrorArea) Clear() {
a.label.SetText("")
+ a.hideFix()
a.box.Hide()
a.mark(false)
}
diff --git a/internal/gui/parts/fields.go b/internal/gui/parts/fields.go
index 9d21ca4..4882abf 100644
--- a/internal/gui/parts/fields.go
+++ b/internal/gui/parts/fields.go
@@ -54,6 +54,16 @@ func (f *Field) Say(message string) { f.area.Say(message) }
// Clear takes the refusal and the mark back together.
func (f *Field) Clear() { f.area.Clear() }
+// Offer puts a button under what this field is complaining about, for a
+// screen that knows how to put it right - the way to a directory whose
+// contents a run refused. On the field rather than on Fields, which stands at
+// its ceiling of methods.
+func (f *Field) Offer(label string, apply func()) { f.area.Offer(label, apply) }
+
+// Offered is the words on the button under what this field is complaining
+// about, or nothing, for a guard.
+func (f *Field) Offered() string { return f.area.Offered() }
+
// Saying is what this field is currently complaining about, for a guard.
func (f *Field) Saying() string { return f.area.Text() }
@@ -235,6 +245,13 @@ func (s *Fields) Add(setting, label, hint string, detail Detail, control fyne.Ca
// alsoSaying. Folded here rather than at the thirty-two call sites, so a
// field that still carries one is not something anybody can write.
explained := alsoSaying(hint, detail)
+ // A box to tick is drawn with its name beside it wherever it comes from -
+ // a screen's own switch or a setting a format declares (the prototype of
+ // 2026-09-23 reached only the first, and a format's Header stayed a
+ // square under its name).
+ if check, is := control.(*Toggle); is {
+ return s.register(setting, label, explained, check, ToggleSaying(label, explained, check))
+ }
return s.register(setting, label, explained, control,
FieldSaying(label, explained, s.required[setting], s.counter(setting, control), control))
}
@@ -273,7 +290,7 @@ func (s *Fields) Named(label string, detail Detail, control fyne.CanvasObject) f
if d, ok := control.(fyne.Disableable); ok {
s.bare = append(s.bare, bareControl{control: d, after: len(s.list)})
}
- return FieldStack(headingRow(label, detail, false), control)
+ return cellOf(FieldStack(headingRow(label, detail, false), control))
}
// AddToggle is a switch, and since 2026-09-15 it is a field like any other: its
@@ -284,6 +301,9 @@ func (s *Fields) Named(label string, detail Detail, control fyne.CanvasObject) f
// name stands beside the square like every other name, so this is Add with no
// special case: WithRing leaves the square alone because it draws its own edge,
// and the switch cannot be refused so its error area never speaks.
+//
+// Its name stands BESIDE the square since the prototype of 2026-09-23 - see
+// ToggleSaying.
func (s *Fields) AddToggle(setting, name, hint string, detail Detail, check *Toggle) fyne.CanvasObject {
return s.Add(setting, name, hint, detail, check)
}
@@ -471,6 +491,9 @@ func (s *Fields) Mark(setting string, err error) bool {
return false
}
f.Say(inTheWordsOnScreen(f, err))
+ if label, apply := fixFor(f, err); apply != nil {
+ f.area.Offer(label, apply)
+ }
return true
}
diff --git a/internal/gui/parts/fixes.go b/internal/gui/parts/fixes.go
new file mode 100644
index 0000000..ef3a725
--- /dev/null
+++ b/internal/gui/parts/fixes.go
@@ -0,0 +1,62 @@
+package parts
+
+import (
+ "errors"
+ "strconv"
+ "strings"
+
+ "github.com/donislawdev/TestingFilesGenerator/internal/core"
+ "github.com/donislawdev/TestingFilesGenerator/internal/format"
+ "github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
+)
+
+// fixFor is the button a refusal can carry under a field, when the refusal
+// itself holds the value that would put it right - or nothing.
+//
+// One refusal today, and it is the one the owner reported from the running
+// window on 2026-09-23: a size below what a format can make says the smallest
+// it CAN make as an exact count of bytes, "14707223 B", and the only way to
+// act on it was to copy eight digits into the box by hand. The refusal is a
+// format.BelowMinimumError, which carries the minimum as a number rather than
+// only as words, so the button is worked out from the number and not read out
+// of the sentence.
+//
+// Nothing is filled in by itself. Untouchable rule 1 says a size is exact or
+// refused, never quietly rounded, and a button somebody presses is the
+// opposite of quiet: the value goes in the box, the box is checked again like
+// any typed value, and the byte count under it says what it is.
+//
+// Only on a box holding one size. A range of two sizes ("1kb-8kb") refused
+// for its lower end would be replaced by a single number, which is a
+// different request, so it gets the sentence and no button.
+//
+// And only on a box that HOLDS the size that was refused, and that is not the
+// same thing as a box holding one size. Under "Around a limit" the box holds
+// the limit and the set is built one byte below it, on it and one above - so
+// a limit of 74 for a PNG refuses a file of 73, and the button put 74 back in
+// the box: the same 73, the same refusal, forever. Found by an outside review
+// of #126 and measured on 2026-09-23 (docs/REVIEW-126-2026-09-23.md).
+//
+// Raising the limit by the shortfall was tried and is not the answer either.
+// PNG makes exactly 74 B or 86 B and more, nothing between, so a limit of 75
+// builds 74, 75 and 76 and two of them fall in the gap - the button walked
+// 74, 75, 86, 87 and called every step "the smallest size". The smallest limit
+// whose three files all exist is a question about the format, and only the
+// engine can answer it (G1). Until it can, the limit keeps the sentence and
+// gets no button, because a button that promises the smallest value and does
+// not deliver it is worse than none.
+func fixFor(f *Field, err error) (string, func()) {
+ var below *format.BelowMinimumError
+ if !errors.As(err, &below) || below.Minimum <= 0 {
+ return "", nil
+ }
+ boxes := boxesIn(f.Control)
+ if len(boxes) != 1 || strings.Contains(boxes[0].Text, "-") {
+ return "", nil
+ }
+ if held, parsed := core.ParseSize(boxes[0].Text); parsed != nil || held != below.Requested {
+ return "", nil
+ }
+ box, value := boxes[0], strconv.FormatInt(below.Minimum, 10)
+ return text.UseSmallestSize(core.ExactBytes(below.Minimum)), func() { box.SetText(value) }
+}
diff --git a/internal/gui/parts/folding.go b/internal/gui/parts/folding.go
index 0605889..4629234 100644
--- a/internal/gui/parts/folding.go
+++ b/internal/gui/parts/folding.go
@@ -1,8 +1,12 @@
package parts
import (
+ "image/color"
+
"fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
@@ -79,13 +83,87 @@ func NewFolding(title string, head []fyne.CanvasObject, content ...fyne.CanvasOb
// and Duplicate, which act on the batch - a section of it is not a thing
// anybody removes or copies on its own.
func NewInnerFolding(title string, content ...fyne.CanvasObject) *Folding {
+ return NewInnerFoldingOf(GroupSettings, title, content...)
+}
+
+// NewInnerFoldingOf is NewInnerFolding for a group of a named kind, which is
+// what colours the rail down its left edge.
+func NewInnerFoldingOf(kind GroupKind, title string, content ...fyne.CanvasObject) *Folding {
// At the rank of a subheading rather than a section's title since
// 2026-09-21: drawn as a section, it read as one (owner, running window).
+ // White like every other heading: the owner's verdict on a coloured title
+ // was that one blue title among white ones looked strange, so the colour
+ // lives in the rail alone.
f := newFolding(title, words(title, TextBody, true, theme.ColorNameForeground), nil, content...)
- f.object = f.inside
+ // Framed, with a rail in the colour of what the group is about, since the
+ // prototype of 2026-09-23 - the owner chose this of three drawn side by
+ // side (wells, bands, accent). Opened, the settings of a format and the
+ // settings of a damage ran into the fields above them and into each other,
+ // and nothing said where one group ended or which was which.
+ //
+ // Less room above and below than at the sides, because the head row keeps
+ // TabInset round its words already for the pointer's fill to draw in.
+ padded := container.New(layout.NewCustomPaddedLayout(GroupInsetY, GroupInsetY, GroupInset, GroupInset), f.inside)
+ rail := canvas.NewRectangle(PaletteColour(groupInk(kind), theme.VariantDark))
+ rail.CornerRadius = RadiusMark
+ f.object = container.New(groupCell{}, container.NewStack(groupFrame(), container.New(leftRail{}, rail), padded))
return f
}
+// GroupKind is what a group of settings is about, and it decides the colour
+// of the rail down the group's left edge.
+type GroupKind int
+
+const (
+ // GroupSettings is a format's own settings - the primary colour.
+ GroupSettings GroupKind = iota
+ // GroupDamage is the settings of a damage - the warning colour, because
+ // what it does to a file is the one thing on the form that breaks it.
+ GroupDamage
+ // GroupNotes is the notes a batch leaves in the manifest - neutral.
+ GroupNotes
+)
+
+// groupFrame is the line drawn round a group of settings inside a section.
+func groupFrame() *canvas.Rectangle {
+ rect := canvas.NewRectangle(color.Transparent)
+ rect.CornerRadius = RadiusPanel
+ rect.StrokeColor = PaletteColour(theme.ColorNameSeparator, theme.VariantDark)
+ rect.StrokeWidth = edgeWidth
+ return rect
+}
+
+// groupInk is the colour of a group's rail.
+func groupInk(kind GroupKind) fyne.ThemeColorName {
+ switch kind {
+ case GroupDamage:
+ return theme.ColorNameWarning
+ case GroupNotes:
+ return ColorNameLabel
+ case GroupSettings:
+ return theme.ColorNamePrimary
+ }
+ return theme.ColorNamePrimary
+}
+
+// leftRail lays its one child as a narrow bar down the left edge.
+type leftRail struct{}
+
+func (leftRail) MinSize([]fyne.CanvasObject) fyne.Size { return fyne.Size{} }
+
+func (leftRail) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ for _, o := range objects {
+ o.Resize(fyne.NewSize(RadiusMark, size.Height))
+ o.Move(fyne.NewPos(0, 0))
+ }
+}
+
+// groupCell marks a group of settings for the grid, which keeps GapSection
+// round it rather than the gap between two rows of fields - measured on the
+// prototype, a group 16 px from the fields above it read as one more row of
+// them. Lays its one child out whole.
+type groupCell struct{ wideCell }
+
func newFolding(title string, titled fyne.CanvasObject, head []fyne.CanvasObject, content ...fyne.CanvasObject) *Folding {
f := &Folding{open: true, title: title}
@@ -93,7 +171,7 @@ func newFolding(title string, titled fyne.CanvasObject, head []fyne.CanvasObject
f.line.Importance = widget.LowImportance
f.line.Hide()
- f.body = Column(GapField, content...)
+ f.body = Grid(content...)
// The title first and the arrow after it, which is not where a disclosure
// arrow usually goes and is not a preference either. An arrow in front of
@@ -115,9 +193,14 @@ func newFolding(title string, titled fyne.CanvasObject, head []fyne.CanvasObject
// the same amount so the title's ink does not move - see overhang.
arrow := widget.NewIcon(theme.MenuDropDownIcon())
f.head = newFoldHead(f, arrow)
- words := Padded(TabInset, container.NewHBox(titled, arrow, quiet(f.line)))
+ // The arrow in FRONT since the prototype of 2026-09-23, reversing the
+ // choice above on the owner's decision from the real window: after the
+ // words it was a 6 px triangle at the end of a heading, and the heading
+ // read as an orphaned subtitle rather than as something that opens. The
+ // arrow now stands on the left edge and the words start after it.
+ words := Padded(TabInset, container.NewHBox(arrow, titled, quiet(f.line)))
f.head.under = words
- row := container.NewBorder(nil, nil, nil, container.NewHBox(head...), container.NewStack(f.head, words))
+ row := container.NewBorder(nil, nil, nil, ButtonRow(head...), container.NewStack(f.head, words))
f.inside = Column(GapField, container.New(overhang{by: TabInset}, row), f.body)
return f
diff --git a/internal/gui/parts/grid.go b/internal/gui/parts/grid.go
new file mode 100644
index 0000000..ea75809
--- /dev/null
+++ b/internal/gui/parts/grid.go
@@ -0,0 +1,364 @@
+package parts
+
+import (
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+)
+
+// Grid lays the fields of a section in GridColumns columns, in the order
+// given, each field taking as many columns as its value needs.
+//
+// Why, measured on 2026-09-23 in the real window at the size the owner's
+// window opens at: the controls of the single batch screen used 37 per cent
+// of their section's width, in three widths (140, 296 and 788 px), and the
+// screen was taller than its window. A first prototype put two fields to a
+// row, each filling its half - and the owner's verdict from the running
+// window was the other half of the same defect: a box holding "1" or a list
+// of "avif" drawn 386 px wide. So a field now takes the fewest columns that
+// hold it and fills exactly those (see fixedWidth), and the right edge of a
+// row is still one line.
+//
+// Anything handed over through Wide takes the whole row, and so does anything
+// that is not a field: a sentence, a list, a fold, a path.
+//
+// A thing with no height takes no row. The boxes a screen refills at run time
+// - the settings a format declares, the parameters of a damage - are empty
+// until something is chosen, and an empty row would still cost a gap.
+//
+// A narrower grid gives a field more columns rather than cutting it, down to
+// one field a row, so the window can be as narrow as its widest field.
+func Grid(items ...fyne.CanvasObject) *fyne.Container {
+ return container.New(&formGrid{}, items...)
+}
+
+// Wide marks something that takes the whole row of a Grid.
+func Wide(o fyne.CanvasObject) fyne.CanvasObject {
+ return container.New(wideCell{}, o)
+}
+
+// IsWide says whether a grid item was marked to take the whole row.
+func IsWide(o fyne.CanvasObject) bool {
+ c, ok := o.(*fyne.Container)
+ if !ok {
+ return false
+ }
+ _, wide := c.Layout.(wideCell)
+ return wide
+}
+
+// wideCell is the mark Wide leaves. It lays its one child out whole, so it
+// is invisible anywhere but in a grid.
+type wideCell struct{}
+
+func (wideCell) MinSize(objects []fyne.CanvasObject) fyne.Size {
+ size := fyne.NewSize(0, 0)
+ for _, o := range objects {
+ if o.Visible() {
+ size = size.Max(o.MinSize())
+ }
+ }
+ return size
+}
+
+func (wideCell) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ for _, o := range objects {
+ o.Resize(size)
+ o.Move(fyne.NewPos(0, 0))
+ }
+}
+
+// fieldCell is the layout of one whole field - its body and the room under it
+// for a refusal - and the mark a Grid reads to put it in a column. Anywhere
+// else it is a column like any other.
+//
+// In a grid the refusal is not drawn in the column. Measured on the prototype
+// of 2026-09-23: "Size "abc" has no number: write something like 10mb or
+// 1048576" broke into three lines in a column 185 px wide and pushed the whole
+// form down. So the grid tells the cell where its refusal goes - under the
+// row, from the field's own left edge to the right edge of the row, and under
+// any refusal about a field before it in the same row - and the cell draws it
+// there. The red edge stays on the box it is about.
+type fieldCell struct {
+ column
+ inGrid bool
+ areaTop float32
+ areaWidth float32
+}
+
+func (f *fieldCell) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ if !f.inGrid || len(objects) < 2 {
+ f.column.Layout(objects, size)
+ return
+ }
+ body, area := objects[0], objects[1]
+ body.Resize(fyne.NewSize(size.Width, body.MinSize().Height))
+ body.Move(fyne.NewPos(0, 0))
+ area.Resize(fyne.NewSize(f.areaWidth, area.MinSize().Height))
+ area.Move(fyne.NewPos(0, f.areaTop))
+}
+
+// cellOf stacks a field's pieces - its body, then its refusal - as one grid
+// cell.
+func cellOf(pieces ...fyne.CanvasObject) fyne.CanvasObject {
+ return container.New(&fieldCell{column: column{gap: GapTight}}, pieces...)
+}
+
+// refusalOf is the grid cell and the refusal area of a field, or nil.
+func refusalOf(o fyne.CanvasObject) (*fieldCell, fyne.CanvasObject) {
+ c, ok := o.(*fyne.Container)
+ if !ok || len(c.Objects) < 2 {
+ return nil, nil
+ }
+ cell, is := c.Layout.(*fieldCell)
+ if !is {
+ return nil, nil
+ }
+ return cell, c.Objects[1]
+}
+
+// hanging lays a list item out: the marker level with the first line of the
+// words, and the words hanging beside it so a second line starts under the
+// first rather than under the marker. Two objects, marker first.
+//
+// The first line's height is asked of the same kind of label the item is
+// drawn with, rather than written down, so it follows the font (GUI rule 14).
+type hanging struct{}
+
+func (hanging) firstLine() float32 { return Prose("Ag").MinSize().Height }
+
+func (h hanging) MinSize(objects []fyne.CanvasObject) fyne.Size {
+ if len(objects) < 2 {
+ return fyne.NewSize(0, 0)
+ }
+ marker, item := objects[0].MinSize(), objects[1].MinSize()
+ return fyne.NewSize(marker.Width+GapLabel+item.Width, fyne.Max(marker.Height, item.Height))
+}
+
+func (h hanging) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ if len(objects) < 2 {
+ return
+ }
+ marker := objects[0].MinSize()
+ objects[0].Resize(marker)
+ objects[0].Move(fyne.NewPos(0, (h.firstLine()-marker.Height)/2))
+ indent := marker.Width + GapLabel
+ objects[1].Resize(fyne.NewSize(size.Width-indent, size.Height))
+ objects[1].Move(fyne.NewPos(indent, 0))
+}
+
+// isGridCell is whether something goes in a column rather than across the
+// row: a field, and nothing else. A sentence or a fold handed over without
+// Wide still takes the row, because half a sentence is never what was meant.
+func isGridCell(o fyne.CanvasObject) bool {
+ c, ok := o.(*fyne.Container)
+ if !ok || len(c.Objects) == 0 {
+ return false
+ }
+ switch c.Layout.(type) {
+ case *fieldCell, *toggleCell:
+ return true
+ }
+ return false
+}
+
+// formGrid is the layout behind Grid. A pointer, because it remembers the
+// width it was last laid out at - see MinSize.
+type formGrid struct{ width float32 }
+
+// placed is one item of a grid: where it starts, how many columns it takes
+// and which row it is in.
+type placed struct {
+ o fyne.CanvasObject
+ row, column int
+ span int
+}
+
+// columnWidth is the width of one of GridColumns columns in a grid this wide.
+func columnWidth(width float32) float32 {
+ return (width - GapColumns*float32(GridColumns-1)) / GridColumns
+}
+
+// spanOf is how many columns an item takes at a given column width: the
+// fewest that hold what it needs, and all of them for anything that is not a
+// field or was marked Wide. Worked out from the item, never written down per
+// field - a translation that makes a name longer makes its field wider.
+func spanOf(o fyne.CanvasObject, column float32) int {
+ if IsWide(o) || !isGridCell(o) {
+ return GridColumns
+ }
+ need := cellWidthNeed(o)
+ for span := 1; span < GridColumns; span++ {
+ if column*float32(span)+GapColumns*float32(span-1) >= need {
+ return span
+ }
+ }
+ return GridColumns
+}
+
+// pack lays the shown items into rows at one width, in the order given: an
+// item that does not fit in what is left of a row starts the next one.
+func pack(objects []fyne.CanvasObject, width float32) []placed {
+ column := columnWidth(width)
+ var out []placed
+ row, used := 0, 0
+ for _, o := range objects {
+ if !o.Visible() || o.MinSize().Height == 0 {
+ continue
+ }
+ span := spanOf(o, column)
+ if used > 0 && used+span > GridColumns {
+ row, used = row+1, 0
+ }
+ out = append(out, placed{o: o, row: row, column: used, span: span})
+ used += span
+ if used >= GridColumns {
+ row, used = row+1, 0
+ }
+ }
+ // Before any height is asked: a box to tick is taller beside a named
+ // field than alone, and only the packing knows which it is.
+ levelToggles(out)
+ return out
+}
+
+// rowHeights is how tall each row of a packing is at one width: its tallest
+// item without refusals, then every refusal about a field in the row, one
+// under another across the row. It tells each field cell where its refusal
+// goes, which is why it takes the width.
+func rowHeights(items []placed, width float32) []float32 {
+ var heights []float32
+ for _, p := range items {
+ for len(heights) <= p.row {
+ heights = append(heights, 0)
+ }
+ body := p.o.MinSize().Height
+ if cell, _ := refusalOf(p.o); cell != nil {
+ body = p.o.(*fyne.Container).Objects[0].MinSize().Height
+ }
+ heights[p.row] = fyne.Max(heights[p.row], body)
+ }
+ column := columnWidth(width)
+ under := make([]float32, len(heights))
+ for _, p := range items {
+ cell, area := refusalOf(p.o)
+ if cell == nil {
+ continue
+ }
+ x := float32(p.column) * (column + GapColumns)
+ cell.inGrid = true
+ cell.areaWidth = width - x
+ cell.areaTop = heights[p.row] + GapTight + under[p.row]
+ if area.Visible() {
+ under[p.row] += area.MinSize().Height + GapTight
+ }
+ }
+ for row := range heights {
+ heights[row] += under[row]
+ }
+ return heights
+}
+
+// MinSize is as wide as the widest single item, because a narrower grid gives
+// an item more columns rather than cutting it, and as tall as the packing at
+// the width this grid was last given. Before its first layout that is the
+// width of the form's column, which is what almost every grid is given.
+//
+// The height depends on the width, which a toolkit minimum cannot say
+// directly - the same thing a wrapping label does, and answered the same way:
+// by remembering the last width.
+func (g *formGrid) MinSize(objects []fyne.CanvasObject) fyne.Size {
+ width := g.width
+ if width <= 0 {
+ width = ColumnWidth - Inset*2
+ }
+ var minWidth, height float32
+ for _, o := range objects {
+ if o.Visible() {
+ minWidth = fyne.Max(minWidth, cellWidthNeed(o))
+ }
+ }
+ items := pack(objects, width)
+ gaps := rowGaps(items)
+ for i, h := range rowHeights(items, width) {
+ height += gaps[i] + h
+ }
+ return fyne.NewSize(minWidth, height)
+}
+
+func (g *formGrid) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ g.width = size.Width
+ column := columnWidth(size.Width)
+ items := pack(objects, size.Width)
+ heights := rowHeights(items, size.Width)
+ gaps := rowGaps(items)
+ tops := make([]float32, len(heights))
+ for i := 1; i < len(heights); i++ {
+ tops[i] = tops[i-1] + heights[i-1] + gaps[i]
+ }
+ for _, p := range items {
+ width := column*float32(p.span) + GapColumns*float32(p.span-1)
+ height := p.o.MinSize().Height
+ if cell, _ := refusalOf(p.o); cell != nil {
+ height = heights[p.row]
+ }
+ p.o.Resize(fyne.NewSize(width, height))
+ p.o.Move(fyne.NewPos(float32(p.column)*(column+GapColumns), tops[p.row]))
+ }
+}
+
+// rowGaps is the room above each row of a packing: none above the first,
+// GapSection next to a group of settings and GapField between rows of fields.
+func rowGaps(items []placed) []float32 {
+ rows := 0
+ grouped := map[int]bool{}
+ for _, p := range items {
+ rows = max(rows, p.row+1)
+ if holdsGroup(p.o) {
+ grouped[p.row] = true
+ }
+ }
+ gaps := make([]float32, rows)
+ for i := 1; i < rows; i++ {
+ gaps[i] = GapField
+ if grouped[i] || grouped[i-1] {
+ gaps[i] = GapSection
+ }
+ }
+ return gaps
+}
+
+// holdsGroup is whether an item is a group of settings, or a box a screen
+// refills that holds one.
+func holdsGroup(o fyne.CanvasObject) bool {
+ if isGroup(o) {
+ return true
+ }
+ c, ok := o.(*fyne.Container)
+ if !ok {
+ return false
+ }
+ for _, child := range c.Objects {
+ if child.Visible() && isGroup(child) {
+ return true
+ }
+ }
+ return false
+}
+
+// isGroup is whether something is a group of settings itself.
+func isGroup(o fyne.CanvasObject) bool {
+ _, group := layoutOf(o).(groupCell)
+ return group
+}
+
+// cellWidthNeed is how wide an item asks to be in a grid. A field asks with
+// its body alone: its refusal is laid under the row, across it, so a long
+// sentence or the button under it must not widen the field's column -
+// measured on the prototype, the button offering the smallest size pushed the
+// size box to two columns and the field after it to the next row.
+func cellWidthNeed(o fyne.CanvasObject) float32 {
+ if cell, _ := refusalOf(o); cell != nil {
+ return o.(*fyne.Container).Objects[0].MinSize().Width
+ }
+ return o.MinSize().Width
+}
diff --git a/internal/gui/parts/ledger.go b/internal/gui/parts/ledger.go
new file mode 100644
index 0000000..7201180
--- /dev/null
+++ b/internal/gui/parts/ledger.go
@@ -0,0 +1,84 @@
+package parts
+
+import (
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+)
+
+// Ledger is a table of three columns of words - on the About screen, what a
+// piece of carried code is, under which licence, and whose it is.
+//
+// The first two columns are as wide as their widest entry, so a module path
+// or a licence identifier stays on one line and the three line up down the
+// table. The last takes what is left and wraps.
+//
+// Unless what is left would be less than a third of the row. Measured on the
+// prototype of 2026-09-23: the one entry shipped beside the program on
+// Windows has a name of 41 characters and a licence of "MIT AND Apache-2.0
+// WITH LLVM-exception AND BSL-1.0", and the copyright beside them broke into
+// seven lines of one or two words. Then the first two give up width in
+// proportion to what they asked for, and wrap as well.
+//
+// Prototype of 2026-09-23. It replaced one line per entry with the three
+// parts told apart by two spaces, which in the real window read as a wall of
+// text in which nothing lined up.
+func Ledger(rows [][3]string) fyne.CanvasObject {
+ var first, second float32
+ for _, r := range rows {
+ first = fyne.Max(first, measured(r[0]))
+ second = fyne.Max(second, measured(r[1]))
+ }
+ shape := &ledgerShape{first: first, second: second}
+ lines := make([]fyne.CanvasObject, 0, len(rows))
+ for _, r := range rows {
+ lines = append(lines, container.New(ledgerRow{shape}, Prose(r[0]), Prose(r[1]), Prose(r[2])))
+ }
+ return Column(GapLabel, lines...)
+}
+
+// measured is how wide a column has to be to hold one entry on one line: the
+// words themselves, which is all a Prose label draws round its ink, and
+// GapInline on top so that rounding a fraction of a pixel cannot push the
+// last word onto a second line.
+func measured(s string) float32 {
+ return fyne.MeasureText(s, TextBody, fyne.TextStyle{}).Width + GapInline
+}
+
+// ledgerShape is the widths the columns of one table asked for, shared by
+// every row so the columns line up.
+type ledgerShape struct{ first, second float32 }
+
+// widths is how wide each column is in a row this wide.
+func (l *ledgerShape) widths(width float32) [3]float32 {
+ room := fyne.Max(0, width-GapColumns*2)
+ first, second := l.first, l.second
+ if least := room / 3; first+second > room-least {
+ scale := (room - least) / (first + second)
+ first, second = first*scale, second*scale
+ }
+ return [3]float32{first, second, fyne.Max(0, room-first-second)}
+}
+
+// ledgerRow lays one row out in the table's columns.
+type ledgerRow struct{ shape *ledgerShape }
+
+func (l ledgerRow) MinSize(objects []fyne.CanvasObject) fyne.Size {
+ height := float32(0)
+ for _, o := range objects {
+ height = fyne.Max(height, o.MinSize().Height)
+ }
+ return fyne.NewSize(NumericWidth*3+GapColumns*2, height)
+}
+
+func (l ledgerRow) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ if len(objects) < 3 {
+ return
+ }
+ widths := l.shape.widths(size.Width)
+ x := float32(0)
+ for i, o := range objects[:3] {
+ o.Resize(fyne.NewSize(widths[i], o.MinSize().Height))
+ o.Move(fyne.NewPos(x, 0))
+ x += widths[i] + GapColumns
+ }
+}
diff --git a/internal/gui/parts/menulook.go b/internal/gui/parts/menulook.go
new file mode 100644
index 0000000..aaa4d22
--- /dev/null
+++ b/internal/gui/parts/menulook.go
@@ -0,0 +1,56 @@
+package parts
+
+import (
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/theme"
+ "fyne.io/fyne/v2/widget"
+)
+
+// CreateRenderer draws the toolkit's menu with its arrow in the primary
+// colour.
+//
+// Why, measured on the owner's screenshot of the running window on
+// 2026-09-23: a menu and a box to type in were the same to the pixel -
+// (57,57,63) inside, (81,81,87) at the edge - and a button was one step
+// lighter at (72,72,78), so a menu could be told from neither. Three ways of
+// drawing it were shown side by side in three real windows - the arrow cut
+// off by a line, an outline with no fill, the arrow in the accent - and the
+// owner chose the accent. It is the one mark on the form that says "this
+// opens a list" and nothing else says it: a box has no arrow, a button has no
+// arrow, and nothing else is drawn in the primary colour at rest but Generate.
+//
+// The toolkit's renderer is kept whole: its objects are a background
+// rectangle, a tap animation, the value and the arrow (fyne v2.8.1
+// widget/select.go, CreateRenderer), and only the arrow's colour changes. It
+// is put back after every refresh, because the toolkit's refresh sets the
+// resource it chose again. A disabled menu keeps the toolkit's disabled arrow,
+// so a menu frozen for a run does not look live.
+func (c *Chooser) CreateRenderer() fyne.WidgetRenderer {
+ look := &menuLook{WidgetRenderer: c.Select.CreateRenderer(), menu: c}
+ look.accent()
+ return look
+}
+
+type menuLook struct {
+ fyne.WidgetRenderer
+ menu *Chooser
+}
+
+func (m *menuLook) Refresh() {
+ m.WidgetRenderer.Refresh()
+ m.accent()
+}
+
+// accent colours the arrow, unless the menu is disabled.
+func (m *menuLook) accent() {
+ if m.menu.Disabled() {
+ return
+ }
+ for _, o := range m.WidgetRenderer.Objects() {
+ if icon, ok := o.(*widget.Icon); ok {
+ icon.Resource = theme.NewColoredResource(theme.MenuDropDownIcon(), theme.ColorNamePrimary)
+ icon.Refresh()
+ return
+ }
+ }
+}
diff --git a/internal/gui/parts/parts.go b/internal/gui/parts/parts.go
index 981ab4a..8017a66 100644
--- a/internal/gui/parts/parts.go
+++ b/internal/gui/parts/parts.go
@@ -195,11 +195,12 @@ func Titled(name, sentence string) fyne.CanvasObject {
// plain container, so anything that walks the tree already knows what it is -
// and a walk that does not know one type stops seeing every field below it,
// which is exactly what happened when cards arrived.
+//
+// The content is a Grid since the prototype of 2026-09-23: fields in
+// GridColumns columns, each taking the fewest columns that hold its value,
+// and anything else across the row.
func Section(title string, content ...fyne.CanvasObject) fyne.CanvasObject {
- body := make([]fyne.CanvasObject, 0, len(content)+1)
- body = append(body, sectionTitle(title))
- body = append(body, content...)
- return container.NewStack(panelSurface(), Padded(Inset, Column(GapField, body...)))
+ return container.NewStack(panelSurface(), Padded(Inset, Column(GapField, sectionTitle(title), Grid(content...))))
}
// FieldColumn stacks fields the way a section stacks them, for the boxes a screen
@@ -317,11 +318,18 @@ func (s shifted) Layout(objects []fyne.CanvasObject, size fyne.Size) {
// The marker is its own column, so a wrapped item hangs under its own text
// rather than under the marker. These items wrap: one of them is a sentence
// about MB against MiB that runs past the width of this card.
+//
+// At the size and colour of ordinary text since the prototype of 2026-09-23.
+// It was drawn as a caption - the smallest rank, in the hint's colour - and
+// measured in the real window that made the most useful words on two screens
+// the faintest: what a preset typically finds, and how to use the program,
+// under a licence drawn in full white. The marker stands level with the FIRST
+// line of its item rather than in the middle of all of them (O235).
func Bullets(items []string) fyne.CanvasObject {
rows := make([]fyne.CanvasObject, 0, len(items))
for _, item := range items {
- marker := words(bulletMarker, TextCaption, false, theme.ColorNamePlaceHolder)
- rows = append(rows, container.NewBorder(nil, nil, marker, nil, Note(item)))
+ marker := words(bulletMarker, TextBody, false, ColorNameLabel)
+ rows = append(rows, container.New(hanging{}, marker, Prose(item)))
}
// Tight, because these items are one list rather than a run of separate
// statements. Measured on 2026-08-20 at the toolkit's padding: 35 px
@@ -486,7 +494,7 @@ func ActionBar(rail fyne.CanvasObject, content ...fyne.CanvasObject) fyne.Canvas
// mistake rather than as an indent.
column := container.New(readableWidth{}, Indented(Column(GapLabel, content...)))
standing := fyne.CanvasObject(column)
- if rail != nil {
+ if rail != nil && len(content) > 0 {
// Laid over the column rather than beside it. Sharing the row, the rail
// would take width from one side only and the buttons the column
// centres would sit off centre by half of it.
@@ -494,11 +502,47 @@ func ActionBar(rail fyne.CanvasObject, content ...fyne.CanvasObject) fyne.Canvas
// The vertical box is what keeps the rail one row tall. Handed straight
// to a stack it would be resized to the whole bar, and a Donate button
// as tall as the bar is what the first attempt drew.
- standing = container.NewStack(column, container.NewVBox(rail))
+ standing = container.New(railOver{centred: content[0]}, column, container.NewVBox(rail))
}
return container.NewStack(panelSurface(), Padded(InsetBar, standing))
}
+// railOver is the rail laid over the column, and a bar that cannot be made
+// narrower than the width at which the two stop meeting.
+//
+// A stack until 2026-09-23, and a stack's minimum is the larger of its two
+// children rather than what they need side by side - so the window could be
+// narrowed until the centred buttons slid under the rail. Measured with
+// guirender on the batch screen: "Add a batch" covered Preview from about
+// 495 px on main and from about 588 px once the buttons grew their gaps on
+// #126, and Preview was gone altogether at 495. Found while checking an
+// outside review of #126 (docs/REVIEW-126-2026-09-23.md). GUI rule 21 asks
+// exactly this of the smallest window.
+//
+// The buttons are centred in the bar, so the room they leave either side is
+// half of what is left over. That half has to hold the rail and a gap, which
+// makes the smallest bar the buttons plus twice the rail and the gap.
+type railOver struct{ centred fyne.CanvasObject }
+
+func (r railOver) MinSize(objects []fyne.CanvasObject) fyne.Size {
+ size := fyne.Size{}
+ for _, o := range objects {
+ size = size.Max(o.MinSize())
+ }
+ if len(objects) > 1 {
+ need := r.centred.MinSize().Width + 2*(objects[1].MinSize().Width+GapColumns)
+ size.Width = fyne.Max(size.Width, need)
+ }
+ return size
+}
+
+func (railOver) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ for _, o := range objects {
+ o.Resize(size)
+ o.Move(fyne.NewPos(0, 0))
+ }
+}
+
// Screen stacks sections under a head - a Title, or a Titled pair.
//
// Windows compose sections rather than laying themselves out in one function.
@@ -597,13 +641,15 @@ func (f fixedWidth) Layout(objects []fyne.CanvasObject, size fyne.Size) {
if len(objects) == 0 {
return
}
- // The width asked for, whatever room the parent offers. Clamping it to the
- // room was tried on 2026-08-25 and taken straight back out: MinSize above
- // already reports this width, so a parent that lays out properly never
- // offers less - and the parents that offer nought are the ones part way
- // through being built, where clamping collapsed every declared setting to a
- // box of nought or minus three pixels. A guard said so within the minute.
- objects[0].Resize(fyne.NewSize(f.width, size.Height))
+ // At least the width asked for, and the whole of the room when the parent
+ // offers more: since the prototype of 2026-09-23 a field stands in a
+ // column of a Grid and fills it, so the right edge of a form is one line.
+ // The width is a floor now, not a size. Clamping it DOWN to the room was
+ // tried on 2026-08-25 and taken straight back out: the parents that offer
+ // nought are the ones part way through being built, where clamping
+ // collapsed every declared setting to a box of nought or minus three
+ // pixels.
+ objects[0].Resize(fyne.NewSize(fyne.Max(f.width, size.Width), size.Height))
objects[0].Move(fyne.NewPos(0, 0))
}
diff --git a/internal/gui/parts/required.go b/internal/gui/parts/required.go
index 055c876..688a5ea 100644
--- a/internal/gui/parts/required.go
+++ b/internal/gui/parts/required.go
@@ -72,9 +72,11 @@ func headingRow(label string, detail Detail, required bool) fyne.CanvasObject {
if detail.Text != "" && detail.on != nil {
row = append(row, newDetailButton(detail))
}
- if len(row) == 1 {
- return head
- }
+ // Always the line, even for a name alone, since the prototype of
+ // 2026-09-23: two fields side by side have to start their boxes on one
+ // height, and a name with no button beside it was a line shorter by the
+ // button - so its box stood higher than its neighbour's. See
+ // headingLine.MinSize.
return container.New(headingLine{}, row...)
}
@@ -119,7 +121,9 @@ func gapBefore(o fyne.CanvasObject) float32 {
}
func (h headingLine) MinSize(objects []fyne.CanvasObject) fyne.Size {
- size := fyne.NewSize(0, 0)
+ // As tall as the explanation button whether or not there is one, so every
+ // name line on a form is one height - see headingRow.
+ size := fyne.NewSize(0, GlyphButton)
first := true
for _, o := range objects {
if !o.Visible() {
diff --git a/internal/gui/parts/ring.go b/internal/gui/parts/ring.go
index 512a846..6da1245 100644
--- a/internal/gui/parts/ring.go
+++ b/internal/gui/parts/ring.go
@@ -365,11 +365,18 @@ func (c *Chooser) FocusGained() {
c.mark()
}
-// mark turns the drawn state on: the toolkit's own first, because the widget's
-// appearance depends on it, and then the edge.
+// mark turns the drawn state on: the edge, and nothing else.
+//
+// It told the toolkit as well until the prototype of 2026-09-23, and the
+// toolkit answers by filling the whole menu with the focus colour
+// (select.go, bgColor) - measured in the real window after Ctrl+P as
+// (26,92,124) across the box, and on two of the owner's screenshots the same
+// day. Every other control here marks the keyboard with a ring standing
+// clear of it, which is what the note on Chooser asks for, so the menu does
+// too. Keys still reach it: the canvas routes them by its own focus, not by
+// the widget's flag.
func (c *Chooser) mark() {
c.marked = true
- c.Select.FocusGained()
if c.ring != nil {
c.ring.Focus(true)
}
diff --git a/internal/gui/parts/togglefield.go b/internal/gui/parts/togglefield.go
new file mode 100644
index 0000000..d0318c9
--- /dev/null
+++ b/internal/gui/parts/togglefield.go
@@ -0,0 +1,118 @@
+package parts
+
+import (
+ "image/color"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/widget"
+)
+
+// ToggleSaying is a switch drawn as a box to tick with its name beside it, on
+// one line: the square, the name, and the button behind which the longer
+// explanation waits.
+//
+// Beside rather than over, which is the one exception to a name standing over
+// its control, and it is the prototype of 2026-09-23 on the owner's choice
+// from the running window. A square alone on a line under its name read as an
+// empty box rather than as something to tick. The name is part of the target:
+// pressing it ticks the box, the way every box to tick on a desktop answers.
+//
+// A cell of a Grid like any field. Beside a field with a name over its box,
+// the square stands level with that box rather than with the name - see
+// toggleCell.
+func ToggleSaying(label string, detail Detail, check *Toggle) Built {
+ line := []fyne.CanvasObject{check, container.NewStack(Heading(label), newNameTap(check))}
+ if detail.Text != "" && detail.on != nil {
+ line = append(line, newDetailButton(detail))
+ }
+ area := NewErrorArea()
+ body := FieldStack(container.New(headingLine{}, line...))
+ return Built{Object: container.New(&toggleCell{}, body, area.Object()), Body: body, Area: area}
+}
+
+// toggleCell is the layout of a box to tick in a grid: a column like any
+// field's, pushed down by the height of a name line when the grid says the
+// row it stands in has a named field in it. A row of boxes to tick alone,
+// or a box alone under a sentence, keeps no such room - measured on the
+// prototype, the room alone read as a hole.
+//
+// The height of a name line is the token headingLine.MinSize answers with,
+// not a measurement.
+type toggleCell struct{ level bool }
+
+func (t *toggleCell) drop() float32 {
+ if t.level {
+ return GlyphButton + GapLabel
+ }
+ return 0
+}
+
+func (t *toggleCell) MinSize(objects []fyne.CanvasObject) fyne.Size {
+ size := column{gap: GapTight}.MinSize(objects)
+ size.Height += t.drop()
+ return size
+}
+
+func (t *toggleCell) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ y := t.drop()
+ for _, o := range objects {
+ if !o.Visible() {
+ continue
+ }
+ height := o.MinSize().Height
+ o.Resize(fyne.NewSize(size.Width, height))
+ o.Move(fyne.NewPos(0, y))
+ y += height + GapTight
+ }
+}
+
+// levelToggles tells every box to tick in a packing whether it shares its
+// row with a named field.
+func levelToggles(items []placed) {
+ named := map[int]bool{}
+ for _, p := range items {
+ if _, field := layoutOf(p.o).(*fieldCell); field {
+ named[p.row] = true
+ }
+ }
+ for _, p := range items {
+ if t, toggle := layoutOf(p.o).(*toggleCell); toggle {
+ t.level = named[p.row]
+ }
+ }
+}
+
+// layoutOf is the layout of a container, or nil for anything else - so a
+// question about what kind of cell something is takes one step, not two.
+func layoutOf(o fyne.CanvasObject) fyne.Layout {
+ if c, ok := o.(*fyne.Container); ok {
+ return c.Layout
+ }
+ return nil
+}
+
+// NameTap lies over the name beside a box to tick and ticks the box when
+// the name is pressed. It draws nothing: the name stays plain words, the same
+// kind every other name on the form is, so whatever reads the names off a
+// screen reads this one too. Exported so that a guard walking the tree can
+// tell it from a control: the name and this over it are a stack of words
+// followed by something, which is the shape of a field.
+type NameTap struct {
+ widget.BaseWidget
+ check *Toggle
+}
+
+func newNameTap(check *Toggle) *NameTap {
+ t := &NameTap{check: check}
+ t.ExtendBaseWidget(t)
+ return t
+}
+
+// Tapped ticks the box the name belongs to.
+func (t *NameTap) Tapped(*fyne.PointEvent) { t.check.Tapped(nil) }
+
+func (t *NameTap) CreateRenderer() fyne.WidgetRenderer {
+ return widget.NewSimpleRenderer(canvas.NewRectangle(color.Transparent))
+}
diff --git a/internal/gui/parts/tokens.go b/internal/gui/parts/tokens.go
index 7c55439..1917759 100644
--- a/internal/gui/parts/tokens.go
+++ b/internal/gui/parts/tokens.go
@@ -179,7 +179,40 @@ const (
// promise, and the owner's report from the running window was the
// obvious one: why are they so long. A path is the one value that can
// be, so a path still takes the row.
- TextWidth = NumericWidth*2 + GapColumns
+ //
+ // One column of the grid since the prototype of 2026-09-23, the same as a
+ // number: in a form of GridColumns columns a name sized for two numbers
+ // took two columns, and the owner's report from the running window was
+ // boxes far wider than what they hold. A placeholder longer than this
+ // still widens it (ShapedFor), and in the grid it fills its columns.
+ TextWidth = NumericWidth
+ // GridColumns is how many columns a section's fields are laid in. Four
+ // gives 185 px a column in the form's 788 px (ColumnWidth less a panel's
+ // inset on each side), which holds a number box with room for a short
+ // menu. Chosen for the prototype of 2026-09-23, not measured against
+ // five - five would leave a number box 4.8 px to spare.
+ GridColumns = 4
+ // GroupInset is the room inside the frame drawn round a group of
+ // settings a section folds away - see NewInnerFolding.
+ GroupInset = space3
+ // GroupInsetY is the same room above and below, smaller because the
+ // head of a fold keeps room of its own round its words.
+ GroupInsetY = space1
+ // BarButtonInsetX and BarButtonInsetY are the room inside a button that
+ // stands in the bar at the foot, around its words. Bigger than the room
+ // inside a field on purpose, prototype of 2026-09-23: measured in the
+ // real window, Generate was 26 px tall - one of the smallest things on a
+ // screen whose whole purpose is that one press. The first prototype gave
+ // it 12 above and below and the owner's verdict from the running window
+ // was "gigantic", so the height is a field's again and only the sides
+ // keep more room: the button is told apart by its width and its colour.
+ BarButtonInsetX = space4
+ BarButtonInsetY = ControlInset
+ // GapButtons is the space between two buttons side by side - in that bar
+ // and in the head of a batch (ButtonRow). It was the toolkit's padding,
+ // about 4 px, so Preview and Generate, and Duplicate and Remove, each read
+ // as one control with two words in it.
+ GapButtons = space3
// GlyphButton is the side of the small square button that holds one
// glyph - the mark beside a field's name that opens its explanation. The
// glyph itself is the toolkit's inline icon, 20, and the square keeps two
diff --git a/internal/gui/text/locale/en.json b/internal/gui/text/locale/en.json
index 6ccd44d..cdfe3fe 100644
--- a/internal/gui/text/locale/en.json
+++ b/internal/gui/text/locale/en.json
@@ -77,7 +77,7 @@
},
"DetailBuildOnPreset": {
"description": "The longer explanation behind the button beside a field name.",
- "other": "Off, the batches below are the whole recipe. On, the chosen preset's files come first and the batches are added after them."
+ "other": "Unticked, the batches below are the whole recipe. Ticked, the chosen preset's files come first and the batches are added after them."
},
"DetailDamage": {
"description": "The longer explanation behind the button beside a field name.",
@@ -224,6 +224,11 @@
"one": "1 file",
"other": "{{.Count}} files"
},
+ "FormatCount": {
+ "description": "Shown in the window. One form per number, and a language may need more than the two English has - add them beside these. Carries one value, {{.Count}}, which has to stay spelled exactly that way.",
+ "one": "1 format",
+ "other": "{{.Count}} formats"
+ },
"HintBasePreset": {
"description": "The line under a field name, saying what the field does.",
"other": "Its files come first."
@@ -318,7 +323,7 @@
},
"NoteBase": {
"description": "Shown in the window.",
- "other": "With the switch on, the chosen preset's files come first and the batches below are added after them. With it off, the batches are the whole recipe."
+ "other": "With the box ticked, the chosen preset's files come first and the batches below are added after them. Without it, the batches are the whole recipe."
},
"NoteManifestOnly": {
"description": "Shown in the window.",
@@ -328,6 +333,10 @@
"description": "Shown in the window.",
"other": "Nothing was produced."
},
+ "PlaceholderLeftEmpty": {
+ "description": "Stands in an empty box, in a quieter colour than a value. Carries one value, {{.Value}}, which has to stay spelled exactly that way.",
+ "other": "default: {{.Value}}"
+ },
"PlaceholderNotStated": {
"description": "Stands in an empty box, in a quieter colour than a value.",
"other": "not stated"
@@ -504,6 +513,10 @@
"description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Count}}, {{.Reason}}.",
"other": "this run asks for {{.Count}} files - {{.Reason}}"
},
+ "UseSmallestSize": {
+ "description": "Shown in the window. Carries one value, {{.Size}}, which has to stay spelled exactly that way.",
+ "other": "Use the smallest size, {{.Size}}"
+ },
"WillGoTo": {
"description": "Shown in the window. Carries one value, {{.Directory}}, which has to stay spelled exactly that way.",
"other": "will go to {{.Directory}}"
diff --git a/internal/gui/text/screens.go b/internal/gui/text/screens.go
index 41a1a76..88752f4 100644
--- a/internal/gui/text/screens.go
+++ b/internal/gui/text/screens.go
@@ -252,6 +252,13 @@ func PresetCatchesHeading() string { return say("PresetCatchesHeading", "Typical
// comes from the size that was asked for.
func PlaceholderWorkedOut() string { return say("PlaceholderWorkedOut", "worked out from the size") }
+// UseSmallestSize is the button under a refusal about a size below what the
+// format can make. It puts the smallest size that works into the box, so the
+// count of bytes in the refusal does not have to be copied by hand.
+func UseSmallestSize(size string) string {
+ return sayf("UseSmallestSize", "Use the smallest size, {{.Size}}", map[string]any{"Size": size})
+}
+
// PlaceholderLeftEmpty stands in a format setting that has a declared default,
// showing what happens if the field is not touched.
//
@@ -260,13 +267,14 @@ func PlaceholderWorkedOut() string { return say("PlaceholderWorkedOut", "worked
// untouchable rule 5 needs that difference - it is what tells a limit somebody
// chose from a placeholder we invented.
//
-// The value alone since 2026-08-12, where it used to read "left empty: 10mb".
-// That the value is what happens when nothing is typed is what a placeholder
-// already means, and the sentence under the field says "default 10mb" in words
-// - so the box was carrying a third copy in the one place that vanishes as soon
-// as anybody uses it.
+// The value alone from 2026-08-12, where it used to read "left empty: 10mb",
+// on the reason that the sentence under the field said "default 10mb" in
+// words. That sentence moved behind the field's explanation button on
+// 2026-08-25, so the box became the only place on the screen saying it - and
+// the owner's review of the prototype of 2026-09-23 found a grey "60" or "1"
+// read as a value somebody had already typed. So the word is back, once.
func PlaceholderLeftEmpty(declared string) string {
- return declared
+ return sayf("PlaceholderLeftEmpty", "default: {{.Value}}", map[string]any{"Value": declared})
}
// ChoiceLeftAlone is gone, and the reason it existed is worth keeping because
@@ -573,7 +581,7 @@ func SectionBase() string { return say("SectionBase", "Build on a preset") }
// screen does not have. Pointed out by an outside review of #119.
func NoteBase() string {
return say("NoteBase",
- "With the switch on, the chosen preset's files come first and the batches below are added after them. With it off, the batches are the whole recipe.")
+ "With the box ticked, the chosen preset's files come first and the batches below are added after them. Without it, the batches are the whole recipe.")
}
// FieldBuildOnPreset names the switch that turns the section on. Off is the
@@ -581,7 +589,7 @@ func NoteBase() string {
func FieldBuildOnPreset() string { return say("FieldBuildOnPreset", "Start from a preset") }
func DetailBuildOnPreset() string {
return say("DetailBuildOnPreset",
- "Off, the batches below are the whole recipe. On, the chosen preset's files come first and the batches are added after them.")
+ "Unticked, the batches below are the whole recipe. Ticked, the chosen preset's files come first and the batches are added after them.")
}
// FieldBasePreset names the box choosing the preset to build on. A different
diff --git a/internal/gui/text/text.go b/internal/gui/text/text.go
index e3bbfd9..7c9953e 100644
--- a/internal/gui/text/text.go
+++ b/internal/gui/text/text.go
@@ -107,7 +107,10 @@ func RunLine(count int, total string, formats []string, destination string) stri
if total != "" {
facts = append(facts, total)
}
- if len(formats) > 0 {
+ switch {
+ case len(formats) > namedFormats:
+ facts = append(facts, formatCount(len(formats)))
+ case len(formats) > 0:
facts = append(facts, Formats(formats))
}
if destination != "" {
@@ -116,6 +119,20 @@ func RunLine(count int, total string, formats []string, destination string) stri
return strings.Join(facts, separator)
}
+// namedFormats is how many kinds of file the line names before it counts them
+// instead. The prototype of 2026-09-23 found a preset's line naming all
+// twenty-six and running to two lines at the foot of the window - a list
+// nobody reads in a place meant to be scanned, and the preset's own settings
+// say which formats it takes. Three is a choice, not a measurement: few
+// enough to take in at a glance.
+const namedFormats = 3
+
+// formatCount is the kinds of file on the line when there are too many to
+// name.
+func formatCount(n int) string {
+ return sayN("FormatCount", "1 format", "{{.Count}} formats", n, nil)
+}
+
// WillGoTo is the destination, as one fact among the others on the line.
func WillGoTo(dir string) string {
return sayf("WillGoTo", "will go to {{.Directory}}", map[string]any{"Directory": dir})
diff --git a/internal/gui/window/about.go b/internal/gui/window/about.go
index e612fe3..7c399cc 100644
--- a/internal/gui/window/about.go
+++ b/internal/gui/window/about.go
@@ -64,7 +64,7 @@ func About(h Host) fyne.CanvasObject {
// In a card like every other block on every other screen, so this reads
// as a page of the application rather than as the one screen that was
// left as it was.
- parts.Section(text.SectionLicence(), parts.Prose(version.LicenceNotice)),
+ parts.Section(text.SectionLicence(), parts.Prose(paragraphs(version.LicenceNotice))),
// The support address, in words, on the one screen somebody reads when
// they are deciding what this program costs them.
//
@@ -77,27 +77,29 @@ func About(h Host) fyne.CanvasObject {
// one use in the whole tree, as the button's destination, so a person
// whose desktop did nothing had nowhere to go. The comment beside
// OpenLink said this screen carried it. This is that screen carrying it.
+ //
+ // The Donate button stands here, in the card, since the prototype of
+ // 2026-09-23 - the bar at the foot of this screen held nothing else, a
+ // whole strip of the window for one quiet word, while this card named
+ // the same address as words nobody could press. The address stays
+ // under the button for the reason above.
parts.Section(text.SectionSupport(),
- parts.Prose(text.DetailDonate()), parts.Prose(text.SupportURL)),
+ parts.Prose(text.DetailDonate()),
+ container.NewHBox(parts.NewButton(parts.Secondary, text.ButtonDonate(), func() { h.OpenLink(text.SupportURL) })),
+ parts.Prose(text.SupportURL)),
)
sections = append(sections, carried()...)
page := parts.Screen(parts.Title(text.HeadingAbout(version.Version)), sections...)
- // The same bar the work screens carry, holding only the Donate button.
- //
- // This screen starts no run and has nothing else to put there, so the bar is
- // almost empty - and it is here anyway, because the button moved into that
- // bar on 2026-08-19 and a button asking for money that is missing from one
- // screen in four is one people conclude they imagined. It is also the screen
- // somebody reads when deciding what this program costs them, which is the
- // worst one to leave it off.
+ // No bar at the foot, since the prototype of 2026-09-23. It held only the
+ // Donate button, which is in the Support card now - so the button is still
+ // on every screen, and this one gives the strip back to the page.
//
// The page is scrolled, which the other three screens have been from the
// start and this one did not need while it held four paragraphs. It holds
// the list of what the binary carries now, and a licence notice that cannot
// be read to the end is the one kind of notice that fails at its only job.
- return container.NewBorder(
- nil, parts.ActionBar(rail(donateButton(h))), nil, nil, container.NewVScroll(page))
+ return container.NewVScroll(page)
}
// carried is what this binary contains that somebody else wrote, read out of
@@ -133,20 +135,54 @@ func carried() []fyne.CanvasObject {
{text.SectionCarriedBeside(), func(i legal.Item) bool { return i.Beside }},
}
for _, g := range groups {
- if lines := carriedLines(items, g.in); lines != "" {
- out = append(out, parts.Section(g.heading, parts.Prose(lines)))
+ if rows := carriedRows(items, g.in); len(rows) > 0 {
+ out = append(out, parts.Section(g.heading, parts.Ledger(rows)))
}
}
return out
}
-// carriedLines writes one group as text, in the order internal/legal settled.
-func carriedLines(items []legal.Item, in func(legal.Item) bool) string {
- var lines []string
+// carriedRows is one group as rows of a table - what it is, under which
+// licence, and whose - in the order internal/legal settled. A table since the
+// prototype of 2026-09-23: the same three things as one line each, told apart
+// by two spaces, read as a wall of text in the real window.
+func carriedRows(items []legal.Item, in func(legal.Item) bool) [][3]string {
+ var rows [][3]string
for _, item := range items {
- if in(item) {
- lines = append(lines, item.Line())
+ if !in(item) {
+ continue
+ }
+ rows = append(rows, [3]string{item.Title(), item.SPDX, item.Copyright})
+ }
+ return rows
+}
+
+// paragraphs joins the lines of each paragraph of a notice written for a
+// terminal, so the window wraps it to its own width instead of drawing a
+// narrow column of lines broken for eighty characters. A short line stays a
+// line of its own - the name and the copyright at the head of the notice are
+// two lines on purpose, and they are the only lines shorter than this.
+//
+// The command line prints the notice as it is written.
+func paragraphs(notice string) string {
+ const wrapped = 40
+ var out []string
+ for _, para := range strings.Split(strings.TrimSpace(notice), "\n\n") {
+ out = append(out, unwrapped(strings.Split(para, "\n"), wrapped))
+ }
+ return strings.Join(out, "\n\n")
+}
+
+// unwrapped joins the lines of one paragraph, keeping a break after any line
+// shorter than wrapped - see paragraphs.
+func unwrapped(lines []string, wrapped int) string {
+ joined := lines[0]
+ for i := 1; i < len(lines); i++ {
+ divider := "\n"
+ if len(lines[i-1]) >= wrapped {
+ divider = " "
}
+ joined += divider + lines[i]
}
- return strings.Join(lines, "\n")
+ return joined
}
diff --git a/internal/gui/window/generate.go b/internal/gui/window/generate.go
index 0d61fe2..f0d14b5 100644
--- a/internal/gui/window/generate.go
+++ b/internal/gui/window/generate.go
@@ -364,7 +364,7 @@ func (g *Generate) settingsSection() []fyne.CanvasObject {
// in recipe keys, and it is what a refusal is matched against.
add := g.fields.Add
return []fyne.CanvasObject{
- parts.Section(text.SectionConfiguration(),
+ g.sections.section(sectionConfiguration, text.SectionConfiguration(),
// The format used to be a section of its own holding one field.
// A grouping of one groups nothing, and it cost a title, a surface
// and two gaps - measured at about 60 px on a screen that was 119
@@ -376,27 +376,26 @@ func (g *Generate) settingsSection() []fyne.CanvasObject {
// moved there, so an empty detail here is what puts that line
// on screen at all.
g.tips.Say(""), g.formatPick),
- // Side by side, because each pair is one thought: how big and how
- // many, then what the group is called and what the files are called.
+ // In the grid's columns since the prototype of 2026-09-23
+ // (parts.Grid): the format, how big, how many and whether damaged
+ // share one row of four, then what the group is called and what
+ // the files are called.
add(format.SettingSize, text.FieldSize(), text.HintSize(), g.tips.Say(text.DetailSize()),
parts.Numeric(g.size)),
add(engine.SettingCount, text.FieldCount(), "", parts.NoDetail, parts.Numeric(g.count)),
+ add(recipe.KeyDamage, text.FieldDamage(), text.HintDamage(),
+ g.tips.Say(text.DetailDamage()), g.damage.pick),
add(engine.SettingID, text.FieldTargetID(), text.HintTargetID(), g.tips.Say(text.DetailTargetID()), parts.Text(g.id)),
add(engine.SettingName, text.FieldNameTemplate(), text.HintNameTemplate(),
g.tips.Say(text.DetailNameTemplate()), parts.Text(g.name)),
// The settings the chosen format declares land here, under the ones
- // every format has.
+ // every format has, across the whole row.
g.propBox,
- // Damage sits under them rather than beside the format, because it
- // is a question about the finished file rather than about which
- // file to make.
- add(recipe.KeyDamage, text.FieldDamage(), text.HintDamage(),
- g.tips.Say(text.DetailDamage()), g.damage.pick),
g.damage.box,
),
- parts.Section(text.SectionOutput(),
- add(engine.SettingOutDir, text.FieldOutputDir(), text.HintOutputDir(), g.tips.Say(text.DetailOutputDir()),
- chooserFor(g.host, g.outDir)),
+ g.sections.section(sectionOutput, text.SectionOutput(),
+ parts.Wide(add(engine.SettingOutDir, text.FieldOutputDir(), text.HintOutputDir(), g.tips.Say(text.DetailOutputDir()),
+ chooserFor(g.host, g.outDir))),
add(engine.SettingSeed, text.FieldSeed(), text.HintSeed(), g.tips.Say(text.DetailSeed()),
parts.Numeric(g.seed)),
g.fields.AddToggle(engine.SettingLabel, text.FieldLabel(), "", g.tips.Say(text.DetailLabel()), g.label),
@@ -521,7 +520,7 @@ func (g *Generate) rebuildDamageFields() {
return
}
- g.damage.fold = parts.NewInnerFolding(text.DamageSettingsFor(d.ID), objects...)
+ g.damage.fold = parts.NewInnerFoldingOf(parts.GroupDamage, text.DamageSettingsFor(d.ID), objects...)
g.damage.fold.OnChange = func(open bool) { g.damage.folded = !open }
g.damage.fold.Set(!g.damage.folded)
g.damage.box.Add(g.damage.fold.Object())
diff --git a/internal/gui/window/open.go b/internal/gui/window/open.go
index c155b58..66a3224 100644
--- a/internal/gui/window/open.go
+++ b/internal/gui/window/open.go
@@ -327,7 +327,8 @@ func FirstScreen(h Host) fyne.CanvasObject {
// program fetches nothing and sends nothing, which is what keeps untouchable
// rule 8 intact - see the carve out written into it on 2026-08-18.
func donateButton(h Host) fyne.CanvasObject {
- return parts.NewButton(parts.Quiet, text.ButtonDonate(), func() { h.OpenLink(text.SupportURL) })
+ // The bar's size, so its words stand level with the buttons beside them.
+ return parts.NewButton(parts.Quiet, text.ButtonDonate(), func() { h.OpenLink(text.SupportURL) }).InTheBar()
}
// chooserFor is the output directory box with a way to browse to one.
diff --git a/internal/gui/window/preset.go b/internal/gui/window/preset.go
index fdb6198..035b656 100644
--- a/internal/gui/window/preset.go
+++ b/internal/gui/window/preset.go
@@ -78,7 +78,9 @@ func NewPreset(host Host, links ...fyne.CanvasObject) *Preset {
// untouchable rule 5 and the one thing this screen must never take away.
p.fields.Require(engine.SettingOutDir, engine.SettingSeed)
- p.paramBox = parts.FieldColumn()
+ // A grid, so each of a preset's settings takes the fewest of the grid's
+ // columns that hold it, like every other field (prototype of 2026-09-23).
+ p.paramBox = parts.Grid()
p.about = parts.FieldColumn()
ids := preset.IDs()
@@ -95,15 +97,15 @@ func NewPreset(host Host, links ...fyne.CanvasObject) *Preset {
nil, nil,
(p.keepScroll(container.NewVScroll(parts.Screen(
parts.Titled(text.TabPresets(), text.SubtitlePreset()),
- parts.Section(text.SectionPreset(),
+ p.sections.section(sectionPreset, text.SectionPreset(),
p.fields.Add(settingPreset, text.FieldPreset(), text.HintPreset(),
p.tips.Say(text.DetailPreset()), p.pick),
p.about,
),
- parts.Section(text.SectionSettings(), p.paramBox),
- parts.Section(text.SectionOutput(),
- p.fields.Add(engine.SettingOutDir, text.FieldOutputDir(), text.HintOutputDir(),
- p.tips.Say(text.DetailOutputDir()), chooserFor(p.host, p.outDir)),
+ p.sections.section(sectionSettings, text.SectionSettings(), p.paramBox),
+ p.sections.section(sectionOutput, text.SectionOutput(),
+ parts.Wide(p.fields.Add(engine.SettingOutDir, text.FieldOutputDir(), text.HintOutputDir(),
+ p.tips.Say(text.DetailOutputDir()), chooserFor(p.host, p.outDir))),
p.fields.Add(engine.SettingSeed, text.FieldSeed(), text.HintSeed(),
p.tips.Say(text.DetailSeed()), parts.Numeric(p.seed)),
),
diff --git a/internal/gui/window/recipe.go b/internal/gui/window/recipe.go
index dbe03c1..22c409e 100644
--- a/internal/gui/window/recipe.go
+++ b/internal/gui/window/recipe.go
@@ -199,7 +199,7 @@ func NewRecipe(host Host, links ...fyne.CanvasObject) *Recipe {
// screen what it is does not depend on scrolling to reach - see rebuild.
// It is disabled with the rest of the form while a run is going, because
// adding a batch mid run would rebuild the form under the run.
- r.addBtn = parts.NewButton(parts.Secondary, text.ButtonAddBatch(), r.addBatch)
+ r.addBtn = parts.NewButton(parts.Secondary, text.ButtonAddBatch(), r.addBatch).InTheBar()
r.runner.busy.also = append(r.runner.busy.also, r.addBtn)
r.body = r.tips.Over(container.NewBorder(
@@ -331,7 +331,7 @@ func (r *Recipe) rebuild() {
// Before the batches, so that Tab walks the screen in the order it is
// read and the order the run takes the targets in.
- r.baseBox.Add(r.base.section(r.fields, r.tips))
+ r.baseBox.Add(r.sections.section(sectionBase, text.SectionBase(), r.base.rows(r.fields, r.tips)...))
panels := make([]fyne.CanvasObject, 0, len(r.batches)+1)
for i, b := range r.batches {
@@ -408,23 +408,28 @@ func (r *Recipe) batchBlock(index int, b *batch) fyne.CanvasObject {
// field label rather than as something to press.
var rows []fyne.CanvasObject
+ // In the grid's columns since the prototype of 2026-09-23 (parts.Grid):
+ // the format, how many and the way of saying how big share the first row,
+ // and the box that way leaves shares the second with what the group is
+ // called and what the files are called.
rows = append(rows,
add(recipe.KeyFormat, text.FieldFormat(), text.HintFormat(),
// Empty for the reason the other screen gives: the button carries
// the line under the label, so it has to exist even when there is
// nothing further to add.
r.tips.Say(""), b.formatPick),
+ add(recipe.KeyCount, text.FieldCount(), "", parts.NoDetail, parts.Numeric(b.count)),
+ )
+ // One way of saying how big, chosen from three, since 2026-08-25.
+ //
+ // They were three boxes side by side with a sentence above them saying
+ // that only one might be filled in - O114, and a sentence because the
+ // screen let somebody fill in two and learn it from a refusal. A switch
+ // takes the state away rather than describing it.
+ rows = append(rows, r.sizeWayFor(b, at, add)...)
+ rows = append(rows,
add(recipe.KeyID, text.FieldTargetID(), text.HintTargetID(),
r.tips.Say(text.DetailTargetID()), parts.Text(b.id)),
- add(recipe.KeyCount, text.FieldCount(), "", parts.NoDetail, parts.Numeric(b.count)),
- // One way of saying how big, chosen from three, since 2026-08-25.
- //
- // They were three boxes side by side with a sentence above them saying
- // that only one might be filled in - O114, and a sentence because the
- // screen let somebody fill in two and learn it from a refusal. A switch
- // takes the state away rather than describing it, and the box it leaves
- // is a full row wide rather than a third of one.
- r.sizeWayFor(b, at, add),
add(recipe.KeyName, text.FieldNameTemplate(), text.HintNameTemplate(),
r.tips.Say(text.DetailNameTemplate()), parts.Text(b.name)),
)
@@ -564,9 +569,9 @@ func (r *Recipe) outputSection() fyne.CanvasObject {
// The manifest name and the seed both say what they fall back to, so this
// section has one box that has to be answered.
r.fields.Require(recipe.KeyOutputDir)
- return parts.Section(text.SectionOutput(),
- r.fields.Add(recipe.KeyOutputDir, text.FieldOutputDir(), text.HintOutputDir(),
- r.tips.Say(text.DetailOutputDir()), chooserFor(r.host, r.outDir)),
+ return r.sections.section(sectionOutput, text.SectionOutput(),
+ parts.Wide(r.fields.Add(recipe.KeyOutputDir, text.FieldOutputDir(), text.HintOutputDir(),
+ r.tips.Say(text.DetailOutputDir()), chooserFor(r.host, r.outDir))),
r.fields.Add(recipe.KeyOutputManifest, text.FieldManifest(), text.HintManifest(),
r.tips.Say(text.DetailManifest()), parts.Text(r.manifest)),
r.fields.Add(recipe.KeySeed, text.FieldSeed(), text.HintSeed(),
diff --git a/internal/gui/window/recipebase.go b/internal/gui/window/recipebase.go
index dc73ee6..5c01875 100644
--- a/internal/gui/window/recipebase.go
+++ b/internal/gui/window/recipebase.go
@@ -110,11 +110,12 @@ func (b *base) choose(id string) error {
// could not produce one, because the last batch had no Remove button.
func (b *base) carriesTheRun() bool { return b.on.Checked }
-// section is the part as it appears on the screen, registered under the
-// keys a refusal about it arrives with: extends for the preset itself, and
+// rows are the part as it appears on the screen, registered under the keys a
+// refusal about it arrives with: extends for the preset itself, and
// with. for each parameter, which is the line in the recipe the value
-// would be written on.
-func (b *base) section(fields *parts.Fields, tips *parts.Tips) fyne.CanvasObject {
+// would be written on. The screen puts them in a section that folds - see
+// sections.go.
+func (b *base) rows(fields *parts.Fields, tips *parts.Tips) []fyne.CanvasObject {
rows := []fyne.CanvasObject{
parts.Note(text.NoteBase()),
fields.AddToggle(settingBuildOnPreset, text.FieldBuildOnPreset(), "",
@@ -125,7 +126,7 @@ func (b *base) section(fields *parts.Fields, tips *parts.Tips) fyne.CanvasObject
tips.Say(text.DetailBasePreset()), parts.Menu(b.pick)))
rows = append(rows, b.parameterRows(fields, tips)...)
}
- return parts.Section(text.SectionBase(), rows...)
+ return rows
}
// parameterRows draws the chosen preset's parameters.
diff --git a/internal/gui/window/recipefolds.go b/internal/gui/window/recipefolds.go
index 8cb9155..670f651 100644
--- a/internal/gui/window/recipefolds.go
+++ b/internal/gui/window/recipefolds.go
@@ -83,7 +83,7 @@ func (r *Recipe) declaredSettings(b *batch, at func(string) string) fyne.CanvasO
// rule at all. TestNothingInTheManifestNotesChangesAByte holds the line against
// the engine rather than against this comment.
func (r *Recipe) manifestNotes(b *batch, add addField) fyne.CanvasObject {
- b.notes = parts.NewInnerFolding(text.SectionManifestNotes(),
+ b.notes = parts.NewInnerFoldingOf(parts.GroupNotes, text.SectionManifestNotes(),
parts.Note(text.NoteManifestOnly()),
add(recipe.KeyGroup, text.FieldGroup(), text.HintGroup(),
r.tips.Say(text.DetailGroup()), parts.Text(b.group)),
diff --git a/internal/gui/window/recipesize.go b/internal/gui/window/recipesize.go
index 80cbc3d..7b62282 100644
--- a/internal/gui/window/recipesize.go
+++ b/internal/gui/window/recipesize.go
@@ -30,7 +30,7 @@ import (
// screen whose subject had to be guessed from what stood next to it.
func (r *Recipe) sizeWayFor(b *batch, at func(string) string,
add func(setting, label, hint string, detail parts.Detail, control fyne.CanvasObject) fyne.CanvasObject,
-) fyne.CanvasObject {
+) []fyne.CanvasObject {
// Half a row wide, like every other short box on this screen. A size is
// "10mb" and the three used to share one row between them, so a box across
// the whole form would be the only wide box holding four characters - and
@@ -68,7 +68,11 @@ func (r *Recipe) sizeWayFor(b *batch, at func(string) string,
boxes = append(boxes, b.sizeBoxes[key])
}
named := r.fields.Named(text.FieldSizeWay(), r.tips.Say(text.DetailSizeWay()), b.sizeWay)
- return parts.FieldColumn(append([]fyne.CanvasObject{named}, boxes...)...)
+ // The switch and the box it leaves are cells of one row since the
+ // prototype of 2026-09-23 - see parts.Grid. All three boxes go in, and
+ // the grid skips the two that are hidden, so the one shown takes the
+ // place beside the switch.
+ return append([]fyne.CanvasObject{named}, boxes...)
}
// newSizeWaySwitch is the control itself, built with the batch rather than with
diff --git a/internal/gui/window/run.go b/internal/gui/window/run.go
index d94c9cc..136431c 100644
--- a/internal/gui/window/run.go
+++ b/internal/gui/window/run.go
@@ -173,6 +173,10 @@ type runner struct {
// about it can be seen. Nil on a screen that folds nothing.
unfold func(string)
+ // sections are the screen's panels, each of which folds away - see
+ // sections.go. Opened by a refusal as unfold is.
+ sections *sections
+
// destination is where this screen would write, asked for rather than
// stored, because the box it comes from is edited after this is wired.
// Read for the line even when the rest of the form does not settle, so
@@ -312,7 +316,7 @@ func (r *runner) refreshLine() {
}
func newRunner(wait later) *runner {
- r := &runner{fields: parts.NewFields(), line: &runLine{}}
+ r := &runner{fields: parts.NewFields(), line: &runLine{}, sections: newSections()}
// Wired once, here, so that a field added later is covered without anybody
// remembering to wire it. See Fields.WhenTypedIn and recheck.
r.fields.WhenTypedIn(r.recheck)
@@ -333,8 +337,8 @@ func newRunner(wait later) *runner {
r.status.Hide()
r.problem = parts.NewErrorArea()
- r.previewBtn = parts.NewButton(parts.Secondary, text.ButtonPreview(), r.onPreview)
- r.generateBtn = parts.NewButton(parts.Primary, text.ButtonGenerate(), r.onGenerate)
+ r.previewBtn = parts.NewButton(parts.Secondary, text.ButtonPreview(), r.onPreview).InTheBar()
+ r.generateBtn = parts.NewButton(parts.Primary, text.ButtonGenerate(), r.onGenerate).InTheBar()
// Three ranks, so the eye lands on the one that does the work: Generate
// filled, Preview plain beside it, Cancel receding until there is something
// to cancel. They were three identical buttons in a row, which is a choice
@@ -350,7 +354,7 @@ func newRunner(wait later) *runner {
// button. The rank it needs is "as pressable as Preview and not competing
// with Generate", and Generate is disabled while this one is showing
// anyway.
- cancel := parts.NewButton(parts.Secondary, text.ButtonCancel(), r.onCancel)
+ cancel := parts.NewButton(parts.Secondary, text.ButtonCancel(), r.onCancel).InTheBar()
cancel.Disable()
cancel.Hide()
r.busy = &busy{fields: r.fields, preview: r.previewBtn, generate: r.generateBtn,
diff --git a/internal/gui/window/runactions.go b/internal/gui/window/runactions.go
index 8c74411..b4e1a87 100644
--- a/internal/gui/window/runactions.go
+++ b/internal/gui/window/runactions.go
@@ -2,8 +2,6 @@ package window
import (
"fyne.io/fyne/v2"
- "fyne.io/fyne/v2/container"
- "fyne.io/fyne/v2/layout"
"github.com/donislawdev/TestingFilesGenerator/internal/gui/parts"
)
@@ -77,8 +75,10 @@ func (r *runner) actions() fyne.CanvasObject {
//
// Kept by the busy state as well, because the toolkit does not lay the
// row out again when a button in it is hidden - see busy.relay.
- r.busy.row = container.NewHBox(
- layout.NewSpacer(), r.previewBtn, r.generateBtn, r.busy.cancel,
- r.offer.folderBtn, r.offer.manifestBtn, layout.NewSpacer())
+ //
+ // A row of ours with a wider gap since the prototype of 2026-09-23 - see
+ // parts.ButtonRow.
+ r.busy.row = parts.ButtonRow(r.previewBtn, r.generateBtn, r.busy.cancel,
+ r.offer.folderBtn, r.offer.manifestBtn)
return r.busy.row
}
diff --git a/internal/gui/window/runoffers.go b/internal/gui/window/runoffers.go
index 05356b7..fcbe632 100644
--- a/internal/gui/window/runoffers.go
+++ b/internal/gui/window/runoffers.go
@@ -1,6 +1,9 @@
package window
import (
+ "errors"
+ "path/filepath"
+
"github.com/donislawdev/TestingFilesGenerator/internal/engine"
"github.com/donislawdev/TestingFilesGenerator/internal/gui/parts"
"github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
@@ -54,13 +57,13 @@ func newOffers(relay func()) *offers {
o.openFolder(o.wroteInto)
}
})
- o.folderBtn.Hide()
+ o.folderBtn.InTheBar().Hide()
o.manifestBtn = parts.NewButton(parts.Secondary, text.ButtonOpenManifest(), func() {
if o.wroteManifest != "" && o.openFile != nil {
o.openFile(o.wroteManifest)
}
})
- o.manifestBtn.Hide()
+ o.manifestBtn.InTheBar().Hide()
return o
}
@@ -108,6 +111,34 @@ func (o *offers) theManifest(path string) {
o.relay()
}
+// inTheWay is the directory a refusal is about when it is about something
+// already in it - a file or a manifest the run will not write over, or
+// another run still writing there - or nothing.
+//
+// Reported by the owner from the running window on 2026-09-23: the refusal
+// named the file and the directory, and the only way to go and look was to
+// find it by hand in a file manager. And it was read in a one line strip at
+// the foot of the window, scrolled, because a path and two sentences do not
+// fit there. The refusal carries the path as a field, so the directory is
+// taken from there rather than from the box on the screen, which may name a
+// different one by the time somebody presses.
+//
+// The window places it under the output directory box rather than the engine
+// addressing it there, and that is on purpose: the command line reads the
+// same address into its machine readable reports, and a refusal about the
+// contents of a directory is not a refusal about a setting in a recipe.
+func inTheWay(err error) string {
+ var collision *engine.CollisionError
+ var busy *engine.RunInProgressError
+ switch {
+ case errors.As(err, &collision):
+ return filepath.Dir(collision.Path)
+ case errors.As(err, &busy):
+ return busy.Dir
+ }
+ return ""
+}
+
// forget takes both offers away when the next run starts.
func (o *offers) forget() {
o.wroteInto = ""
diff --git a/internal/gui/window/runrefuse.go b/internal/gui/window/runrefuse.go
index c78f21b..b146a91 100644
--- a/internal/gui/window/runrefuse.go
+++ b/internal/gui/window/runrefuse.go
@@ -5,6 +5,7 @@ import (
"strings"
"github.com/donislawdev/TestingFilesGenerator/internal/core"
+ "github.com/donislawdev/TestingFilesGenerator/internal/engine"
"github.com/donislawdev/TestingFilesGenerator/internal/gui/parts"
"github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
)
@@ -27,24 +28,24 @@ import (
// here, which is a second copy of rules the engine owns and the copy that
// drifts.
func (r *runner) refuse(err error) {
+ // A refusal replaces what the status line said about work under way. Seen
+ // in the real window on 2026-09-23: a preview refused for a manifest
+ // already in the directory left "Working out what this would cost..."
+ // standing over the refusal, as if it were still working. Both ways into
+ // a refusal from planning - a preview and Generate - set that line first
+ // and neither took it down.
+ showOn(r.status, "")
var loose []string
// The first box a refusal lands on, so the form can be brought to it. A
// refusal that marks a box the person cannot see reads as a button that did
// nothing - see parts.Reveal and O107.
first := ""
for _, one := range spread(err) {
- // An interface rather than a case per error type, so a screen shown a
- // kind of refusal nobody thought about here still gets it placed. The
- // engine, the format registry and the preset package all answer this
- // and none of them had to be imported for the question to be asked.
- var about interface{ AboutSetting() string }
- if errors.As(one, &about) && about.AboutSetting() != "" {
- if where := r.placeOf(about.AboutSetting()); r.fields.Mark(where, one) {
- if first == "" {
- first = where
- }
- continue
+ if where := placed(r, one); where != "" {
+ if first == "" {
+ first = where
}
+ continue
}
// About the run rather than about one box, or about a setting this
// screen does not draw. The foot of the form is where those belong.
@@ -70,16 +71,56 @@ func (r *runner) refuse(err error) {
// 2026-08-18 answered rather than dodged: refusals about a batch that is
// not on the screen were the reason a list with one batch open at a time
// was rejected.
- if r.unfold != nil {
- for _, marked := range r.fields.Marked() {
+ for _, marked := range r.fields.Marked() {
+ if r.unfold != nil {
r.unfold(marked)
}
+ if field := r.fields.Lookup(marked); field != nil {
+ r.sections.openHolding(field.Control)
+ }
}
if field := r.fields.Lookup(first); field != nil {
parts.Reveal(r.scroll, field.Control)
}
}
+// placed puts one refusal under the box it is about and says which, or says
+// nothing when it is about no box this screen draws. A function rather than a
+// method, because the runner stands at its ceiling of methods, and out of
+// refuse because the two ways of placing nested it past the ceiling of depth.
+func placed(r *runner, one error) string {
+ // About what is already in the output directory: put under that box,
+ // with the way to the directory under the sentence - see inTheWay.
+ if dir := inTheWay(one); dir != "" {
+ if where := r.placeOf(engine.SettingOutDir); r.fields.Mark(where, one) {
+ r.fields.Lookup(where).Offer(text.ButtonOpenFolder(), openIn(r.offer, dir))
+ return where
+ }
+ }
+ // An interface rather than a case per error type, so a screen shown a
+ // kind of refusal nobody thought about here still gets it placed. The
+ // engine, the format registry and the preset package all answer this
+ // and none of them had to be imported for the question to be asked.
+ var about interface{ AboutSetting() string }
+ if !errors.As(one, &about) || about.AboutSetting() == "" {
+ return ""
+ }
+ if where := r.placeOf(about.AboutSetting()); r.fields.Mark(where, one) {
+ return where
+ }
+ return ""
+}
+
+// openIn is what the button under a refusal about a directory does: ask the
+// desktop to show that directory.
+func openIn(o *offers, dir string) func() {
+ return func() {
+ if o.openFolder != nil {
+ o.openFolder(dir)
+ }
+ }
+}
+
// spread opens a refusal that carries several into the ones it carries.
//
// The window used to mark ONE box however many were wrong, because everything
diff --git a/internal/gui/window/sections.go b/internal/gui/window/sections.go
new file mode 100644
index 0000000..53a6040
--- /dev/null
+++ b/internal/gui/window/sections.go
@@ -0,0 +1,62 @@
+package window
+
+import (
+ "fyne.io/fyne/v2"
+
+ "github.com/donislawdev/TestingFilesGenerator/internal/gui/parts"
+)
+
+// The keys a section is remembered under. Names of places, never shown - kept
+// as constants so that nothing a person reads is a literal on a screen.
+const (
+ sectionConfiguration = "configuration"
+ sectionOutput = "output"
+ sectionPreset = "preset"
+ sectionSettings = "settings"
+ sectionBase = "base"
+)
+
+// sections are the panels of a work screen, each of which folds away - the
+// prototype of 2026-09-23, on the owner's word from the running window that
+// File configuration should fold like a batch does, and then that every
+// section of a form should, so that they all look alike.
+//
+// Separate from the runner, because two things have to outlive the panel
+// they are about and the runner is already at its ceiling. Whether a section
+// is folded survives a rebuild - the batch screen builds its panels again
+// whenever a batch is added or removed, and a section that sprang open each
+// time would be the defect Recipe.wire was written against. And a refusal
+// about a box inside a folded section opens it, the same promise every other
+// fold on these screens keeps (see runner.unfold): a box that is marked and
+// cannot be seen reads as a button that did nothing.
+//
+// A folded section says nothing on its head line. What it holds is said at
+// the foot of the screen already - the format, the size, how many and where
+// they go - and a second summary of the same things one panel up would be the
+// same sentence twice.
+type sections struct {
+ folded map[string]bool
+ folds map[string]*parts.Folding
+}
+
+func newSections() *sections {
+ return §ions{folded: map[string]bool{}, folds: map[string]*parts.Folding{}}
+}
+
+// section builds one panel under a key that stays the same across rebuilds.
+func (s *sections) section(key, title string, content ...fyne.CanvasObject) fyne.CanvasObject {
+ fold := parts.NewFolding(title, nil, content...)
+ fold.OnChange = func(open bool) { s.folded[key] = !open }
+ fold.Set(!s.folded[key])
+ s.folds[key] = fold
+ return fold.Object()
+}
+
+// openHolding opens every section a control stands in.
+func (s *sections) openHolding(control fyne.CanvasObject) {
+ for _, fold := range s.folds {
+ if fold.Holds(control) {
+ fold.Set(true)
+ }
+ }
+}
diff --git a/internal/legal/carried.go b/internal/legal/carried.go
index 95a390f..a3acb96 100644
--- a/internal/legal/carried.go
+++ b/internal/legal/carried.go
@@ -64,17 +64,24 @@ type Item struct {
// A licence cannot be absent - Carried lists only what the registry knows, and
// the registry cannot hold an entry without one.
func (i Item) Line() string {
- line := i.Name
- if i.Version != "" {
- line += " " + i.Version
- }
- line += " " + i.SPDX
+ line := i.Title() + " " + i.SPDX
if i.Copyright != "" {
line += " " + i.Copyright
}
return line
}
+// Title is what an item is: its name, and its version where it has one, two
+// spaces apart like the rest of Line. Its own method since 2026-09-23, when
+// the window started drawing these as a table and needed the first column
+// without the other two - values only, for the reason Line gives.
+func (i Item) Title() string {
+ if i.Version == "" {
+ return i.Name
+ }
+ return i.Name + " " + i.Version
+}
+
// Carried reports what this build contains, from the build's own record.
//
// The main module is left out on purpose: it is this program, its licence is