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 @@ - + - - + + @@ -24,5253 +24,5993 @@ - - - - + + + + Button - - - - - - primary at rest + + + + + + + primary at rest + + + + + + + + Generate - - - - - - Generate - - - - - - - - primary under the pointer + + + + + + primary under the pointer + + + + + + + + Generate - - - - - - Generate - - - - - - - - primary pressed + + + + + + primary pressed + + + + + + + + Generate - - - - - - Generate - - - - - - - - primary holding the keyboard + + + + + + primary holding the keyboard + + + + + + + + Generate - - - - - - Generate - - - - - - - - secondary at rest + + + + + + secondary at rest + + + + + + + + Generate - - - - - - Generate - - - - - - - - secondary under the pointer + + + + + + secondary under the pointer + + + + + + + + Generate - - - - - - Generate - - - - - - - - secondary pressed + + + + + + secondary pressed + + + + + + + + Generate - - - - - - Generate - - - - - - - - secondary holding the keyboard + + + + + + secondary holding the keyboard + + + + + + + + Generate - - - - - - Generate - - - - - - - - secondary disabled + + + + + + secondary disabled + + + + + + + + Generate - - - - - - Generate - - - - - - - - quiet at rest + + + + + + quiet at rest + + + + + + + + Generate - - - - - - Generate - - - - - - - - quiet under the pointer + + + + + + quiet under the pointer + + + + + + + + Generate - - - - - - Generate - - - - - - - - quiet pressed + + + + + + quiet pressed + + + + + + + + Generate - - - - - - Generate - - - - - - - - quiet holding the keyboard + + + + + + quiet holding the keyboard + + + + + + + + Generate - - - - - - Generate - - - - - - - - quiet disabled + + + + + + quiet disabled + + + + + + + + Generate - - - - - - Generate - - - - - - - - long text + + + + + + long text + + + + + + + + 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 - - - - - - - - glyph at rest + + + + + + glyph at rest + + + + + + + + - - - - - - - - - - - - - - glyph under the pointer + + + + + + glyph under the pointer + + + + + + + + - - - - - - - - - - - - - - glyph holding the keyboard + + + + + + glyph holding the keyboard + + + + + + + + - - - - - - - - - - - - - - glyph disabled + + + + + + glyph disabled + + + + + + + + - - - - - - - - + Chooser - - - - - - at rest + + + + + + + at rest + - - - - - - - - - - (Select one) - - - - - - - - - - - - - - chosen - - - - - - - - - - - avif - - - - - - - - - - - - - - holding the keyboard - - - - - - + + + - + (Select one) - + - - - - - - - - refused + + + + + + chosen + - - - - - + + + - (Select one) + avif - + - - - - - - - - disabled + + + + + + holding the keyboard + - + + + + + + + + + + (Select one) + + + + + + + + - - - - - - - - (Select one) - - - + + + + + + + refused + - + + + + + + + + + + (Select one) + + + + + + + + - - - - - - - long value + + + + + + disabled + - + + + + + + + + + (Select one) + + + + + + - - - - - - - - Write a label inside each generated file, including the ones that are far too small to hold it + + + + + + + long value + - - + + + + + + + + + Write a label inside each generated file, including the ones that are far too small to hold it + + + + - + - + Entry - - - - - - empty, with its hint - - - - - - - - - - - 10mb + + + + + + + empty, with its hint + - - + + + + + + + + + 10mb + + + + - - - - - - - - typed + + + + + + + typed + - - - - - - - - - - 2048 + + + + + + + + + 2048 + - - - - - - - - holding the keyboard + + + + + + + holding the keyboard + - - - - - - - - - - 2048 + + + + + + + + + 2048 + + - - - - - - - - - disabled + + + + + + + disabled + - - - - - - - - - - 2048 + + + + + + + + + 2048 + - - - - - - - - long text + + + + + + + long text + - - - - - - - - - - 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 + - + - + Toggle - - - - - - off + + + + + + + off + - - - - - - - - + - - - - - - - - on - + + + + + - - - - - - - - - - - - - - - under the pointer + + + + + + on + + + + + + + + + - - - - - - - - - - - - - - holding the keyboard + + + + + + under the pointer + + + + + + + + - - - - - - - - - - - - - - - disabled + + + + + + holding the keyboard + + + + + + + + + - - - - - - + + + + + + + disabled + + + + + + + + + + - + Segments - - - - - - first chosen + + + + + + + first chosen + + + + + + + + + + + Exact + Range + Boundary + - - - - - - - - - Exact - Range - Boundary - - - - - - - - - middle chosen + + + + + + middle chosen + + + + + + + + + + Exact + Range + Boundary + - - - - - - - - Exact - Range - Boundary - - - - - - - - - holding the keyboard + + + + + + holding the keyboard + + + + + + + + + + + Exact + Range + Boundary + - - - - - - - - - Exact - Range - Boundary - - - - - - - - - the last one under the pointer + + + + + + the last one under the pointer + + + + + + + + + + + Exact + Range + Boundary + - - - - - - - - - Exact - Range - Boundary - - - - - - - - - frozen, middle chosen + + + + + + frozen, middle chosen + + + + + + + + + + Exact + Range + Boundary + - - - - - - - - Exact - Range - Boundary - - - - - - - - - long words + + + + + + long words + + + + + + + + + + + Exactly this size + Somewhere in a range + On a boundary + - - - - - - - - - Exactly this size - Somewhere in a range - On a boundary - - - + OpenList - - - - - - a few values, one chosen + + + + + + + a few values, one chosen + - - - - - - - - - - - - - - - - - png + + + + + + + + + + + + + + + + png + - - - - - - jpg + + + + + jpg + - - - - - avif + + + + avif + - - - - - - - - - - - - - - - - - - - - - - - - - - nothing chosen yet - - - - - - - - - - - - - - - - - - png + + - - - - - jpg + + - - - - - avif + + - - - - - - - - - - - + + - - - - - + + + + + - - - - - - - more values than a short window shows at once + + + + + + nothing chosen yet + - - - - - - - - - - - - - - - - - avif + + + + + + + + + + + + + + + + png + - - - - - bmp + + + + jpg + - - - - - csv + + + + avif + - - - - - docx + + - - - - - gif + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - + + + + + - - - - - - - every value with the kind of thing it is + + + + + + more values than a short window shows at once + - - - - - - - - - - - - - - - - - - avif - - - - - - - bmp + + + + + + + + + + + + + + + + avif + - - - - - - csv + + + + bmp + - - - - - - - docx + + + + csv + - - - - - - gif + + + + docx + - - - - - - html + + + + gif + - - - - - - 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 + + + + + + + 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