Skip to content

Commit af761b2

Browse files
donislawdevclaude
andauthored
gui: the window says where things begin and end (#116)
* gui: the window says where things begin and end - the owner's list from the running window The owner's report from the running window: the whole thing runs together, nothing says where a section ends, where a box begins or what is a button. Measured on the shot, every structural surface sat within 1.0 to 1.8:1 of its neighbour, and the palette's role table had classed the boundary between a panel and the page as decoration with no threshold. Two whole looks were built and shown and turned down. What ships is the list the owner gave, each point shown in the running window before it was kept: - a section draws a line round its edge again, in the separator's colour (the guard that forbade the line now requires it, and the fill under it still has to clear the page); - a field's name stands over its box rather than beside it, in an ink a step under the value's, and the byte count goes under the box - so the column of names, the widest-name arithmetic, the hand kept list of every name and the guard holding that list complete all go, because a width nothing draws is a number waiting to be wrong; - Preview, Choose, Duplicate and Add a batch stand on the button's own surface, brighter than a box to type in, asked for by name and by distance; - a fold inside a section is titled at the rank of a subheading, and the pointer lights only its words rather than the whole row; - a field's explanation opens with an edge and a shade below it. Turned down and written up rather than left half in: a menu raised like a button, a brighter edge round a box to type in, both looks built from guidelines or from other applications measured on this machine. Guards: nine went red in one run of the package and each was rewritten to what the window does now, two assertions were added for the new behaviour (the fold's fill narrower than its row, the name's ink readable and a step under the value), 26 stored screen pictures were regenerated, the type ceiling followed the widest type down to 26 methods. Mutation entries: six re-aimed, two removed with the column of names, three added, every pattern found once. Not run locally by the owner's decision: the full suite and the full mutation run. Run: the cheap whole-tree gates plus every guard of every touched file (121, green), gofmt, vet, lint, staticcheck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * gui: after the outside review of #116 - two guards bounded from both sides, the tip's shade in the palette Three findings, all three true on the code as it stood: - the fold head's fill was asked to be narrower than its row and wider than nothing, which a one pixel fill satisfies. It is now asked to be at least as wide as the title it lights and narrower than the row; - the pairing of a name with the box under it was measured once, between the first two fields, and applied to every field - a later pair could drift while the first still held. Each name is now measured against the next name down the screen; - the shade under an explanation was a colour written at the call site. It is a name of the palette now, in both variants - its own name rather than the toolkit's Shadow, which this palette answers with nothing on purpose since 2026-08-24. Run: the guards of the touched files and the cheap whole-tree gates (41), gofmt, vet, lint, staticcheck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent e1c068e commit af761b2

77 files changed

Lines changed: 2817 additions & 2970 deletions

Some content is hidden

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

‎CHANGELOG.md‎

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

1717
### Changed
1818

19+
- **The window says where things begin and end.** Every field's name now
20+
stands above its box rather than beside it, in a lighter ink than the
21+
value under it, so a form reads as a column of named boxes. A section
22+
draws a line round its edge again. Preview, Choose, Duplicate and Add a
23+
batch stand on a surface of their own, brighter than a box to type in, so
24+
a button no longer looks like a field. A folded block of settings inside
25+
a section is titled at the rank of a subheading rather than a section, and
26+
the pointer lights only its words rather than the whole row. A field's
27+
explanation opens with an edge and a shadow, so it reads as something laid
28+
over the form rather than a patch of it.
1929
- **Nine things the owner saw in the running window.** A field's
2030
explanation opens on the same raised surface as an open list, so it no
2131
longer lies flat on the section it covers. A list of formats keeps its

‎internal/guard/controlstates_test.go‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,17 @@ func TestASecondaryButtonWearsAFaceAtRest(t *testing.T) {
271271
return nil
272272
}
273273
rest := faceOf()
274-
if want := parts.PaletteColour(theme.ColorNameInputBackground, theme.VariantDark); rest != want {
275-
t.Errorf("at rest the face is %v and should be the surface of a box to type in, %v - an outline round nothing reads as a bordered word", rest, want)
274+
// The button's own surface since 2026-09-21, and a step brighter than a
275+
// box to type in: the owner's report from the running window was that a
276+
// button wearing the field's surface read as a field. Asked as a
277+
// relationship, not only as a name, so a palette edit that lowers the
278+
// button under the field goes red here rather than on the screen.
279+
if want := parts.PaletteColour(theme.ColorNameButton, theme.VariantDark); rest != want {
280+
t.Errorf("at rest the face is %v and should be the button's own surface, %v - a button in the field's colour reads as a field", rest, want)
281+
}
282+
field := parts.PaletteColour(theme.ColorNameInputBackground, theme.VariantDark)
283+
if gap := lightnessGap(rest, field); gap < 5 {
284+
t.Errorf("the button's face is %.1f L* off a box to type in, and 5 is the least that tells a thing to press from a thing to type in", gap)
276285
}
277286
b.MouseIn(&desktop.MouseEvent{})
278287
hovered := faceOf()

‎internal/guard/detailpopup_test.go‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -252,21 +252,37 @@ func TestTheExplanationFloatsOnTheSurfaceAnOpenListDoes(t *testing.T) {
252252
t.Fatal("hovering the button put nothing on the sheet, so there is no box to measure")
253253
}
254254

255-
var surface *canvas.Rectangle
255+
// Two rectangles since 2026-09-21: the shade the box casts, then the
256+
// surface it stands on. The owner's report from the running window was
257+
// that a flat box with no edge read as a random rectangle, so the
258+
// surface wears a line and a shade shows below it. Each is asked for by
259+
// what it is rather than by its place in the tree.
260+
var surface, shade *canvas.Rectangle
261+
want := parts.PaletteColour(theme.ColorNameMenuBackground, theme.VariantDark)
256262
walk(box, func(o fyne.CanvasObject) {
257-
if rect, is := o.(*canvas.Rectangle); is && surface == nil {
263+
rect, is := o.(*canvas.Rectangle)
264+
if !is {
265+
return
266+
}
267+
if rect.FillColor == want && surface == nil {
258268
surface = rect
269+
} else if _, _, _, a := rect.FillColor.RGBA(); a > 0 && a < 0xFFFF && shade == nil {
270+
shade = rect
259271
}
260272
})
261273
if surface == nil {
262-
t.Fatal("the explanation's box draws no rectangle, so it stands on nothing this guard can measure")
263-
}
264-
want := parts.PaletteColour(theme.ColorNameMenuBackground, theme.VariantDark)
265-
if surface.FillColor != want {
266-
t.Errorf("the explanation stands on %v and an open list on %v - a box the colour of the panel it opens over has no edge anywhere",
267-
surface.FillColor, want)
274+
t.Fatal("the explanation's box draws no rectangle in the colour of an open list, so it stands on nothing that floats")
268275
}
269276
if surface.CornerRadius != parts.RadiusField {
270277
t.Errorf("the explanation's corner is %.0f and a floating control's is %d", surface.CornerRadius, parts.RadiusField)
271278
}
279+
if surface.StrokeWidth == 0 {
280+
t.Error("the explanation's surface has no line round it, which is the random rectangle the owner saw")
281+
}
282+
if shade == nil {
283+
t.Error("the explanation casts no shade, so nothing says it lies over the form rather than in it")
284+
} else if shade.Position().Y <= surface.Position().Y {
285+
t.Errorf("the shade sits at y=%.0f and the surface at y=%.0f - a shade that is not below the box it belongs to reads as a smudge",
286+
shade.Position().Y, surface.Position().Y)
287+
}
272288
}

‎internal/guard/foldhead_test.go‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,23 @@ func TestTheHeadRowDrawsItsStatesAndTheArrowFollows(t *testing.T) {
197197
if want := parts.PaletteColour(theme.ColorNameHover, theme.VariantDark); back.FillColor != want {
198198
t.Errorf("under the pointer the row's fill is %v, not the hover colour %v", back.FillColor, want)
199199
}
200+
// The fill is as wide as the words and no wider, since 2026-09-21: the
201+
// owner's report from the running window was a hover the width of the
202+
// form, which is enormous. The row is still the target - the head is as
203+
// wide as the row - so the two widths are asked for apart: the head wide,
204+
// its fill narrow.
205+
// Bounded from both sides, after the outside review of #116: "narrower
206+
// than the row" alone would have let a one pixel fill through. The fill
207+
// has to be at least as wide as the title it lights, and narrower than
208+
// the row it stands in.
209+
title, ok := labelBox(fold.Object(), "Notes for the manifest")
210+
if !ok {
211+
t.Fatal("the fold's title is not on the screen, so there is nothing to measure the fill against")
212+
}
213+
if row, fill := head.Size().Width, back.Size().Width; fill >= row || fill < title.Width {
214+
t.Errorf("under the pointer the fill is %.0f px wide, the title %.0f and the head %.0f - the fill has to cover the words and stop short of the row",
215+
fill, title.Width, row)
216+
}
200217
if arrow.Resource.Name() == restingArrow {
201218
t.Error("the arrow is inked the same under the pointer as at rest, so it does not follow the row")
202219
}

‎internal/guard/guitext_test.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ var notWords = map[string]string{
108108
`"%s: %w"`: "how one error is wrapped around another, both already worded",
109109
`"•"`: "the marker in front of a list item, a shape rather than a word",
110110
`"panel"`: "our name for a colour, in the palette the toolkit asks by name",
111+
`"label"`: "our name for the ink of a field's name, a colour the palette holds",
112+
`"tipshade"`: "our name for the shade under an explanation, a colour the palette holds",
111113
`"lift"`: "our name for what the pointer does to the filled button, a colour the palette holds",
112114
`"shade"`: "our name for what a press does to the filled button, a colour the palette holds",
113115
`"fyneDo"`: "a migration flag the toolkit reads, never shown",

‎internal/guard/namescolumn_test.go‎

Lines changed: 0 additions & 104 deletions
This file was deleted.

‎internal/guard/palette_test.go‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ func TestThePaletteMeetsTheContrastItWasComputedFor(t *testing.T) {
5050
}
5151
}
5252

53+
// A field's name, since 2026-09-21 in an ink of its own a step under
54+
// the value's, is still read - on the panel it stands on, which is
55+
// lighter than the page and so the harder of the two.
56+
panel := parts.PaletteColour(parts.ColorNamePanel, variant.v)
57+
if got := contrast(parts.PaletteColour(parts.ColorNameLabel, variant.v), panel); got < 4.5 {
58+
t.Errorf("%s: a field's name is %.2f:1 against the panel, under the 4.5 a reader needs", variant.name, got)
59+
}
60+
// And it is a step under the value, or the two inks are one ink with
61+
// two names - the owner accepted the quieter name on 2026-09-21 and
62+
// then asked for it brighter, so the step is small and it is asked
63+
// for as a step rather than as a number.
64+
if label, value := parts.PaletteColour(parts.ColorNameLabel, variant.v), parts.PaletteColour(theme.ColorNameForeground, variant.v); lightnessGap(label, value) < 3 {
65+
t.Errorf("%s: a field's name is %.1f L* off the value under it, so nothing tells the name from the value", variant.name, lightnessGap(label, value))
66+
}
67+
5368
// Recognised as a state: 3.0, from WCAG 1.4.11. What carries it is the
5469
// LINE round a control - parts.Ring - and not the focus colour, which
5570
// is a wash the toolkit lays over whatever the control already is.

‎internal/guard/samename_test.go‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,24 @@ func TestNoSectionIsNamedAfterAFieldInsideIt(t *testing.T) {
7272
}
7373
}
7474

75+
// fieldNamesOn is every field name a screen draws, read off the fields
76+
// themselves - a field is the one container laid out as a field, and its
77+
// first thing is its name. Asked by the layout rather than by the weight of
78+
// the words, for the reason the guard above gives.
79+
func fieldNamesOn(screen fyne.CanvasObject) []string {
80+
var out []string
81+
walk(screen, func(obj fyne.CanvasObject) {
82+
box, ok := obj.(*fyne.Container)
83+
if !ok || !parts.IsField(box) || len(box.Objects) < 2 {
84+
return
85+
}
86+
if name, named := headingOf(box.Objects[0]); named && name != "" {
87+
out = append(out, name)
88+
}
89+
})
90+
return out
91+
}
92+
7593
// And the preset card still says what it is for.
7694
//
7795
// The half that stops the guard above being satisfied by deleting the title.

‎internal/guard/sectionsurface_test.go‎

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,28 +65,30 @@ func TestASectionDrawsItsOwnSurface(t *testing.T) {
6565
subject.what, surface.FillColor, want)
6666
}
6767

68-
// No line round it, and the protection that line used to give has moved
69-
// rather than gone. Both halves matter and they are checked together.
68+
// A line round it, since 2026-09-21, and the fill under it - both, and
69+
// both are checked.
7070
//
71-
// It used to be drawn with one, because the fill on its own was 4.0 L*
72-
// off the page - a surface you sense rather than see - and a panel with
73-
// no boundary is where this started. What changed on 2026-08-23 is that
74-
// the same one pixel line was also what every box to type in used to
75-
// say "your value goes here". One mark meaning two things means
76-
// neither, and the field is the one that needs it.
77-
//
78-
// So the fill has to do the work alone now, and that is asserted here
79-
// rather than assumed: a border removed without lifting the surface
80-
// would leave exactly the panel this file was written about.
81-
if surface.StrokeWidth != 0 {
82-
t.Errorf("%s draws a line round itself %.1f px wide.\n"+
83-
"Reason: a border is what a box to type in uses, so a container wearing one makes the mark mean nothing.\n"+
84-
"What to do: let the surface group by being a surface.", subject.what, surface.StrokeWidth)
71+
// The line was taken away on 2026-08-23 on the argument that the same
72+
// one pixel line was what a box to type in used to say "your value
73+
// goes here", and a mark meaning two things means neither. The owner's
74+
// report from the running window a month later was the other half of
75+
// that trade: with the fill 5.9 L* off the page and nothing round it,
76+
// the whole window ran together and nobody could tell where a section
77+
// ended. The line is back on the owner's decision, in the separator's
78+
// colour, and the field keeps its own edge in its own colour - two
79+
// marks, two colours, and the surface still does its share.
80+
if surface.StrokeWidth == 0 {
81+
t.Errorf("%s draws no line round itself.\n"+
82+
"Reason: the fill alone was measured at 5.9 L* off the page and read as nothing, 2026-09-21.\n"+
83+
"What to do: stroke the surface in the separator's colour, one edge wide.", subject.what)
84+
}
85+
if want := parts.PaletteColour(theme.ColorNameSeparator, theme.VariantDark); surface.StrokeColor != want {
86+
t.Errorf("%s draws its edge in %v and the palette says a separator is %v", subject.what, surface.StrokeColor, want)
8587
}
8688
page := parts.PaletteColour(theme.ColorNameBackground, theme.VariantDark)
8789
if gap := lightnessGap(surface.FillColor, page); gap < 5 {
88-
t.Errorf("%s is %.1f L* off the page with no line round it, and 5 is the least that reads as a surface.\n"+
89-
"Reason: the edge used to carry this and no longer does, so the fill is all there is.", subject.what, gap)
90+
t.Errorf("%s is %.1f L* off the page, and 5 is the least that reads as a surface even with a line round it.\n"+
91+
"Reason: the line says where the edge is, the fill says there is a thing inside it.", subject.what, gap)
9092
}
9193
}
9294
}

0 commit comments

Comments
 (0)