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

Filter by extension

Filter by extension

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

### Changed

- **The window says where things begin and end.** Every field's name now
stands above its box rather than beside it, in a lighter ink than the
value under it, so a form reads as a column of named boxes. A section
draws a line round its edge again. Preview, Choose, Duplicate and Add a
batch stand on a surface of their own, brighter than a box to type in, so
a button no longer looks like a field. A folded block of settings inside
a section is titled at the rank of a subheading rather than a section, and
the pointer lights only its words rather than the whole row. A field's
explanation opens with an edge and a shadow, so it reads as something laid
over the form rather than a patch of it.
- **Nine things the owner saw in the running window.** A field's
explanation opens on the same raised surface as an open list, so it no
longer lies flat on the section it covers. A list of formats keeps its
Expand Down
13 changes: 11 additions & 2 deletions internal/guard/controlstates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,17 @@ func TestASecondaryButtonWearsAFaceAtRest(t *testing.T) {
return nil
}
rest := faceOf()
if want := parts.PaletteColour(theme.ColorNameInputBackground, theme.VariantDark); rest != want {
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)
// The button's own surface since 2026-09-21, and a step brighter than a
// box to type in: the owner's report from the running window was that a
// button wearing the field's surface read as a field. Asked as a
// relationship, not only as a name, so a palette edit that lowers the
// button under the field goes red here rather than on the screen.
if want := parts.PaletteColour(theme.ColorNameButton, theme.VariantDark); rest != want {
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)
}
field := parts.PaletteColour(theme.ColorNameInputBackground, theme.VariantDark)
if gap := lightnessGap(rest, field); gap < 5 {
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)
}
b.MouseIn(&desktop.MouseEvent{})
hovered := faceOf()
Expand Down
32 changes: 24 additions & 8 deletions internal/guard/detailpopup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,21 +252,37 @@ func TestTheExplanationFloatsOnTheSurfaceAnOpenListDoes(t *testing.T) {
t.Fatal("hovering the button put nothing on the sheet, so there is no box to measure")
}

var surface *canvas.Rectangle
// Two rectangles since 2026-09-21: the shade the box casts, then the
// surface it stands on. The owner's report from the running window was
// that a flat box with no edge read as a random rectangle, so the
// surface wears a line and a shade shows below it. Each is asked for by
// what it is rather than by its place in the tree.
var surface, shade *canvas.Rectangle
want := parts.PaletteColour(theme.ColorNameMenuBackground, theme.VariantDark)
walk(box, func(o fyne.CanvasObject) {
if rect, is := o.(*canvas.Rectangle); is && surface == nil {
rect, is := o.(*canvas.Rectangle)
if !is {
return
}
if rect.FillColor == want && surface == nil {
surface = rect
} else if _, _, _, a := rect.FillColor.RGBA(); a > 0 && a < 0xFFFF && shade == nil {
shade = rect
}
})
if surface == nil {
t.Fatal("the explanation's box draws no rectangle, so it stands on nothing this guard can measure")
}
want := parts.PaletteColour(theme.ColorNameMenuBackground, theme.VariantDark)
if surface.FillColor != want {
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",
surface.FillColor, want)
t.Fatal("the explanation's box draws no rectangle in the colour of an open list, so it stands on nothing that floats")
}
if surface.CornerRadius != parts.RadiusField {
t.Errorf("the explanation's corner is %.0f and a floating control's is %d", surface.CornerRadius, parts.RadiusField)
}
if surface.StrokeWidth == 0 {
t.Error("the explanation's surface has no line round it, which is the random rectangle the owner saw")
}
if shade == nil {
t.Error("the explanation casts no shade, so nothing says it lies over the form rather than in it")
} else if shade.Position().Y <= surface.Position().Y {
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",
shade.Position().Y, surface.Position().Y)
}
}
17 changes: 17 additions & 0 deletions internal/guard/foldhead_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,23 @@ func TestTheHeadRowDrawsItsStatesAndTheArrowFollows(t *testing.T) {
if want := parts.PaletteColour(theme.ColorNameHover, theme.VariantDark); back.FillColor != want {
t.Errorf("under the pointer the row's fill is %v, not the hover colour %v", back.FillColor, want)
}
// The fill is as wide as the words and no wider, since 2026-09-21: the
// owner's report from the running window was a hover the width of the
// form, which is enormous. The row is still the target - the head is as
// wide as the row - so the two widths are asked for apart: the head wide,
// its fill narrow.
// Bounded from both sides, after the outside review of #116: "narrower
// than the row" alone would have let a one pixel fill through. The fill
// has to be at least as wide as the title it lights, and narrower than
// the row it stands in.
title, ok := labelBox(fold.Object(), "Notes for the manifest")
if !ok {
t.Fatal("the fold's title is not on the screen, so there is nothing to measure the fill against")
}
if row, fill := head.Size().Width, back.Size().Width; fill >= row || fill < title.Width {
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",
fill, title.Width, row)
}
if arrow.Resource.Name() == restingArrow {
t.Error("the arrow is inked the same under the pointer as at rest, so it does not follow the row")
}
Expand Down
2 changes: 2 additions & 0 deletions internal/guard/guitext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ var notWords = map[string]string{
`"%s: %w"`: "how one error is wrapped around another, both already worded",
`"•"`: "the marker in front of a list item, a shape rather than a word",
`"panel"`: "our name for a colour, in the palette the toolkit asks by name",
`"label"`: "our name for the ink of a field's name, a colour the palette holds",
`"tipshade"`: "our name for the shade under an explanation, a colour the palette holds",
`"lift"`: "our name for what the pointer does to the filled button, a colour the palette holds",
`"shade"`: "our name for what a press does to the filled button, a colour the palette holds",
`"fyneDo"`: "a migration flag the toolkit reads, never shown",
Expand Down
104 changes: 0 additions & 104 deletions internal/guard/namescolumn_test.go

This file was deleted.

15 changes: 15 additions & 0 deletions internal/guard/palette_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ func TestThePaletteMeetsTheContrastItWasComputedFor(t *testing.T) {
}
}

// A field's name, since 2026-09-21 in an ink of its own a step under
// the value's, is still read - on the panel it stands on, which is
// lighter than the page and so the harder of the two.
panel := parts.PaletteColour(parts.ColorNamePanel, variant.v)
if got := contrast(parts.PaletteColour(parts.ColorNameLabel, variant.v), panel); got < 4.5 {
t.Errorf("%s: a field's name is %.2f:1 against the panel, under the 4.5 a reader needs", variant.name, got)
}
// And it is a step under the value, or the two inks are one ink with
// two names - the owner accepted the quieter name on 2026-09-21 and
// then asked for it brighter, so the step is small and it is asked
// for as a step rather than as a number.
if label, value := parts.PaletteColour(parts.ColorNameLabel, variant.v), parts.PaletteColour(theme.ColorNameForeground, variant.v); lightnessGap(label, value) < 3 {
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))
}

// Recognised as a state: 3.0, from WCAG 1.4.11. What carries it is the
// LINE round a control - parts.Ring - and not the focus colour, which
// is a wash the toolkit lays over whatever the control already is.
Expand Down
18 changes: 18 additions & 0 deletions internal/guard/samename_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ func TestNoSectionIsNamedAfterAFieldInsideIt(t *testing.T) {
}
}

// fieldNamesOn is every field name a screen draws, read off the fields
// themselves - a field is the one container laid out as a field, and its
// first thing is its name. Asked by the layout rather than by the weight of
// the words, for the reason the guard above gives.
func fieldNamesOn(screen fyne.CanvasObject) []string {
var out []string
walk(screen, func(obj fyne.CanvasObject) {
box, ok := obj.(*fyne.Container)
if !ok || !parts.IsField(box) || len(box.Objects) < 2 {
return
}
if name, named := headingOf(box.Objects[0]); named && name != "" {
out = append(out, name)
}
})
return out
}

// And the preset card still says what it is for.
//
// The half that stops the guard above being satisfied by deleting the title.
Expand Down
38 changes: 20 additions & 18 deletions internal/guard/sectionsurface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,30 @@ func TestASectionDrawsItsOwnSurface(t *testing.T) {
subject.what, surface.FillColor, want)
}

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