From dacce4668497f0eff8f65904ec23d2f6cb820021 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Fri, 25 Sep 2026 00:35:33 +0200 Subject: [PATCH 1/3] Make two tests fail when a copy or a plan title goes wrong, and fix the README's row menu The row menu test computed what "Copy everything" should hold with the very function it was testing, so a copy that dropped the display name and the description still passed. It now asks for the fixture's own words. The plan panel's contrast test read TextBlock.Text, which is empty for a block built from runs, so the title and the steps were never measured. It now reads every run in the colour it ends up with, and checks that the title and a step were among what it measured. The README still listed the two copy items the row menu no longer has, and called the third list Everything where the window says All. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 ++-- tests/Bws.Gui.Tests/CopyingAndPanelGuards.cs | 5 ++++ tests/Bws.Gui.Tests/PlanViewGuards.cs | 29 ++++++++++++++------ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9c13943..40ab2a5 100644 --- a/README.md +++ b/README.md @@ -491,12 +491,12 @@ the third ends the process. `BetterWindowsServices.exe`, one argument it knows: `--catalogue` opens a developer's sheet that shows every component of the window in every state, and reads nothing from your machine. -Three lists on the switch above the search box - *Services*, *Drivers*, *Everything* - each saying +Three lists on the switch above the search box - *Services*, *Drivers*, *All* - each saying how big it is. The search box takes the query language and suggests as you type. The filter buttons write into the box. *Columns* chooses what the list shows, a right-click on a column heading narrows the list to that value or puts the column away, and the layout you leave is the layout it -opens in. A row's menu previews every operation before offering it, and copies the name, the -display name, the description or everything. *Export...* writes the rows on screen, in the columns +opens in. A row's menu previews every operation before offering it, and copies the name or +everything about the entry, display name and description included. *Export...* writes the rows on screen, in the columns you have on and the order you sorted them into, to a CSV file. Ctrl+C over the list copies everything about the chosen entry. Escape backs out of the innermost thing first. *Donate*, at the right end of the row above the search box, opens the project's support page in your browser - and diff --git a/tests/Bws.Gui.Tests/CopyingAndPanelGuards.cs b/tests/Bws.Gui.Tests/CopyingAndPanelGuards.cs index ed4ccae..6f9b553 100644 --- a/tests/Bws.Gui.Tests/CopyingAndPanelGuards.cs +++ b/tests/Bws.Gui.Tests/CopyingAndPanelGuards.cs @@ -90,6 +90,11 @@ public async Task The_context_menu_copies_what_each_item_promises() ("gui.menu.copyAll", WpfHost.On(() => Copying.Everything(Picked(window)))) }; + // THE FIXTURE'S OWN WORDS, NOT Copying's - a wanted text computed by the function under test + // agrees with it whatever it drops, and these two fields lost their items to this one. + Assert.Contains("Print Spooler", promises[1].Wanted, StringComparison.Ordinal); + Assert.Contains("Spooler description", promises[1].Wanted, StringComparison.Ordinal); + foreach (var (key, wanted) in promises) { if (string.IsNullOrWhiteSpace(wanted)) diff --git a/tests/Bws.Gui.Tests/PlanViewGuards.cs b/tests/Bws.Gui.Tests/PlanViewGuards.cs index e961c63..3f84d3c 100644 --- a/tests/Bws.Gui.Tests/PlanViewGuards.cs +++ b/tests/Bws.Gui.Tests/PlanViewGuards.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Windows; using System.Windows.Controls; +using System.Windows.Documents; using System.Windows.Media; using Bws.Core.Planning; using Bws.Gui.ViewModels; @@ -331,6 +332,7 @@ public void It_takes_no_focus_of_its_own_and_still_takes_clicks() public async Task Every_word_on_the_panel_can_be_read_on_the_surface_it_is_drawn_on() { var window = await Ready(); + var model = WpfHost.On(() => (MainViewModel)window.DataContext); Assert.True(await WpfHost.On(() => window.Preview(ActionKind.Stop))); WpfHost.Settled(); @@ -353,21 +355,30 @@ public async Task Every_word_on_the_panel_can_be_read_on_the_surface_it_is_drawn var found = new List(); Collect(window.PlanPanel, found); - var words = found.Where(text => !string.IsNullOrWhiteSpace(text.Text)).ToList(); + // THE RUNS, NOT TextBlock.Text, which is EMPTY for a block built from runs - and since + // 2026-09-24 the title and every step are (review of PR 20). A run is measured in the + // colour it ended up with, its block's unless it names its own. + var runs = found + .SelectMany(text => text.Inlines.OfType()) + .Where(run => !string.IsNullOrWhiteSpace(run.Text)) + .ToList(); - return (words - .Select(text => (text.Text, Ratio(Ink(text), surface))) + return (runs + .Select(run => (run.Text, Ratio(Ink(run.Foreground), surface))) .Where(pair => pair.Item2 < 4.5) .Select(pair => string.Create( CultureInfo.InvariantCulture, $"{pair.Item2:F2} {pair.Text}")) - .ToList(), words.Count); + .ToList(), found.Select(text => string.Concat(text.Inlines.OfType().Select(run => run.Text))).ToList()); }); // A GUARD SATISFIED BY ABSENCE IS SATISFIED FOR AS LONG AS NOBODY BUILDS ANYTHING, and this // project has that lesson written in three other files. The panel shows a title, a name, a - // state, a heading and a step at the very least. - Assert.True(read >= 5, $"Only {read} lines were found on the panel, so nothing was measured."); + // state, a heading and a step at the very least - and the title and a step by name, because + // those two are the blocks built from runs. + Assert.True(read.Count(line => !string.IsNullOrWhiteSpace(line)) >= 5, "Too few lines were found on the panel, so nothing was measured."); + Assert.Contains(WpfHost.On(() => model.Planned.Heading.Text), read); + Assert.Contains(WpfHost.On(() => model.Planned.Steps[0].Text), read); Assert.True( thin.Count == 0, @@ -379,9 +390,9 @@ public async Task Every_word_on_the_panel_can_be_read_on_the_surface_it_is_drawn WpfHost.On(window.Close); } - /// The colour a TextBlock ended up with, however it got there. - private static Color Ink(TextBlock text) => - text.Foreground is SolidColorBrush brush ? brush.Color : Colors.Black; + /// The colour a run ended up with, however it got there. + private static Color Ink(Brush ink) => + ink is SolidColorBrush brush ? brush.Color : Colors.Black; /// Every TextBlock under something, including the ones a template built. private static void Collect(DependencyObject from, List into) From 22ffd844ebb0c28fe52db9d47bd4535cc6f98598 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Fri, 25 Sep 2026 09:22:48 +0200 Subject: [PATCH 2/3] Keep the window still while typing, redraw the search box, and set the column headings apart Typing no longer moves the window. A mistake in the query or a note about something still being read took a line of its own under the search box from the first character, which moved the whole window by a line. The short form of the message now stands at the right end of the box, and while the keyboard is in the box a panel under it shows every message in full, wrapped, over the filters. Leaving the box puts the panel away. A test holds the search row to one height whatever it says. The search box is one frame with a magnifier, the text and the message side by side, so the text never runs under the message. It was darker than the window and its focus line took the accent colour of Windows. It now has a lighter fill and a blue focus line, the blue of the tab underline. The red edge of a mistake is drawn by the frame. The field's styles are in a file of their own, Themes/SearchField.xaml. The column headings are semibold, in a secondary grey, with a line under them. The size and weight set on the heading never reached its text, because the heading's TextBlock took the implicit style. A disabled button has no fill, and an enabled one has a slightly stronger fill, through the library's own brush keys, so it reaches every button without a style of its own. The state and startup type marks are 10 instead of 8. The numbers on the overview light up under the pointer with the panel's hover brush, which can be seen on a card. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 12 ++ src/Bws.Gui/App.xaml | 2 + src/Bws.Gui/MainWindow.xaml | 1 + src/Bws.Gui/OverviewView.xaml | 33 +-- src/Bws.Gui/SearchRow.xaml | 189 +++++++++++------- src/Bws.Gui/Themes/Colours.xaml | 8 + src/Bws.Gui/Themes/List.xaml | 15 ++ src/Bws.Gui/Themes/Overview.xaml | 28 ++- src/Bws.Gui/Themes/SearchField.xaml | 175 ++++++++++++++++ src/Bws.Gui/Themes/Spacing.xaml | 10 + src/Bws.Gui/Themes/Surfaces.xaml | 69 +++++++ src/Bws.Gui/Themes/Text.xaml | 13 ++ src/Bws.Gui/Themes/Values.xaml | 21 +- .../AppearanceGuards.cs | 6 +- tests/Bws.Gui.Tests/AnswerLineGuards.cs | 48 ++++- tests/Bws.Gui.Tests/ContrastFloors.cs | 11 +- tests/Bws.Gui.Tests/WpfHost.cs | 4 +- 17 files changed, 532 insertions(+), 113 deletions(-) create mode 100644 src/Bws.Gui/Themes/SearchField.xaml diff --git a/CHANGELOG.md b/CHANGELOG.md index cb29594..3ddac5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,17 @@ is not part of this repository. - "Show the list" on the machine overview is a blue button, so it is easy to find the first time the window opens. - In the plan panel, the name of the entry stands out in bold in the title and in every step. +- The search box has a magnifier and a lighter fill, and a blue line under it while you type in + it. Before, it was darker than the window, and the line took the accent colour of your Windows. +- A mistake in the query, or a note about something still being read, no longer takes a line under + the search box, so the window no longer moves when you start typing. While you are in the box, + a panel under it shows every message in full, over the filters. When you leave the box, the box + keeps its red edge and a short form of the message at its right end. +- The column headings are semibold and grey, with a line under them, so they no longer look like + one more row. +- A button that cannot be pressed has no fill, and one that can has a slightly lighter fill than + before. +- The state and startup type marks in the list and in the details panel are a little larger. ### Removed @@ -83,6 +94,7 @@ is not part of this repository. ### Fixed - The details panel no longer stays open over the machine overview. +- The numbers on the machine overview light up under the pointer where you can see it. - Opening the details panel on another entry no longer shows the previous entry's "no longer in the listing" notice. diff --git a/src/Bws.Gui/App.xaml b/src/Bws.Gui/App.xaml index 3a482d6..1fd9d5d 100644 --- a/src/Bws.Gui/App.xaml +++ b/src/Bws.Gui/App.xaml @@ -107,6 +107,8 @@ templates to add - point 9 of `docs/11` 2.14. --> + + diff --git a/src/Bws.Gui/OverviewView.xaml b/src/Bws.Gui/OverviewView.xaml index 69e3bf6..ddee35e 100644 --- a/src/Bws.Gui/OverviewView.xaml +++ b/src/Bws.Gui/OverviewView.xaml @@ -118,37 +118,8 @@ Click="AskThis" ToolTip="{Binding Query}"> - - - - - + - - + + + + + + + + + + + + - + - - - - - - + + + + + + + + + + + + + + + + Width="{Binding ActualWidth, ElementName=SearchField}"> - - - - - + + + + + + + + + + diff --git a/src/Bws.Gui/Themes/Colours.xaml b/src/Bws.Gui/Themes/Colours.xaml index 22e670e..1fdf4ab 100644 --- a/src/Bws.Gui/Themes/Colours.xaml +++ b/src/Bws.Gui/Themes/Colours.xaml @@ -132,6 +132,14 @@ --> #A0A0A0 + + #CCCCCC + + + + + diff --git a/src/Bws.Gui/Themes/SearchField.xaml b/src/Bws.Gui/Themes/SearchField.xaml new file mode 100644 index 0000000..f6db98d --- /dev/null +++ b/src/Bws.Gui/Themes/SearchField.xaml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Bws.Gui/Themes/Spacing.xaml b/src/Bws.Gui/Themes/Spacing.xaml index 80e0e7e..3f46b30 100644 --- a/src/Bws.Gui/Themes/Spacing.xaml +++ b/src/Bws.Gui/Themes/Spacing.xaml @@ -84,6 +84,16 @@ --> 16,0,0,0 + + 10,0,6,0 + 0,4,8,4 + 0,0,10,0 + 0,4,0,0 + 0,8,0,0 0,0,0,8 + #4A4A4A + + + + + + + + + + + + + + + diff --git a/src/Bws.Gui/Themes/Text.xaml b/src/Bws.Gui/Themes/Text.xaml index 943846e..ca5572f 100644 --- a/src/Bws.Gui/Themes/Text.xaml +++ b/src/Bws.Gui/Themes/Text.xaml @@ -97,6 +97,19 @@ + + + - 8 + 10 0,1,0,0 + + 0,0,0,1 + + + 14 + + + 420 + + + 0,0,0,2 + + + 0 + 420 diff --git a/tests/Bws.Architecture.Tests/AppearanceGuards.cs b/tests/Bws.Architecture.Tests/AppearanceGuards.cs index 21b2f0f..fcc33fb 100644 --- a/tests/Bws.Architecture.Tests/AppearanceGuards.cs +++ b/tests/Bws.Architecture.Tests/AppearanceGuards.cs @@ -161,7 +161,11 @@ public sealed class AppearanceGuards // Actions.xaml, 2026-09-23: the write buttons of the action bar and the shield they // wear. Controls.xaml stood two lines under its ceiling. Added here in the same edit as // the file, for the reason every entry here gives. - "Controls.xaml", "Menus.xaml", "Actions.xaml", "Suggestions.xaml", + // + // SearchField.xaml, 2026-09-25: the search field's frame, text box, sentence and panel, + // out of Suggestions.xaml when the size ratchet counted it among the files near the + // ceiling. Added here in the same edit as the file. + "Controls.xaml", "Menus.xaml", "Actions.xaml", "Suggestions.xaml", "SearchField.xaml", // PlanLines.xaml left Plan.xaml on 2026-09-07 when the markup ratchet fired on THAT // one - a failure that can be escalated needed a template of its own. Added here in diff --git a/tests/Bws.Gui.Tests/AnswerLineGuards.cs b/tests/Bws.Gui.Tests/AnswerLineGuards.cs index ea2c315..e8ce49c 100644 --- a/tests/Bws.Gui.Tests/AnswerLineGuards.cs +++ b/tests/Bws.Gui.Tests/AnswerLineGuards.cs @@ -147,21 +147,59 @@ private static TextBlock Line(MainWindow window, string automationId) => /// /// The search row drawn at 1000 by 200, the picture left in artifacts/gui, and the problem - /// colour counted inside the box's own rectangle - the line under the box wears the same red, - /// and a count over the whole row would pass on the sentence alone. + /// colour counted in a strip down the LEFT EDGE of the field's frame. + /// + /// The edge rather than the field, since 2026-09-25. The frame draws the red edge now, not + /// the text box's error template - the text box is the middle third of the field - and the short + /// sentence at the field's right end wears the same red, so a count over the whole field would + /// pass on the sentence alone. Nothing but the edge is red at the left: the magnifier is grey. /// private static int RedInsideTheBox(MainWindow window, string state) { var drawn = Drawn.Of(window.Search, 1000, 200); - var box = drawn.Around(window.Search.Box); + var frame = drawn.Around(window.Search.SearchField); - Assert.True(box.Width > 0 && box.Height > 0, "the search box has no rectangle, so it was never laid out"); + Assert.True(frame.Width > 0 && frame.Height > 0, "the search field has no rectangle, so it was never laid out"); drawn.Save($"search-row-{state}-1000x200.png"); - return drawn.Count(WpfHost.Declared("MeaningRejected"), box); + return drawn.Count(WpfHost.Declared("MeaningRejected"), new Int32Rect(frame.X, frame.Y, EdgeStrip, frame.Height)); } + /// How far in from the frame's left edge the red is counted - the edge and its antialiasing, short of the magnifier. + private const int EdgeStrip = 4; + + /// + /// THE SEARCH ROW KEEPS ITS HEIGHT WHATEVER THE ANSWER SAYS - owner, 2026-09-25: the window jumped + /// while he typed. tools/gui-probe/typing-jump.ps1 measured it at 31 device pixels on the first + /// character: the sentence about the query took a line of its own under the box. It stands in the + /// field now, and the full text in a panel that measures to nothing, so a row that grows with what + /// the answer says is that fault back. Asked with the longest thing it can say - a mistake, whose + /// sentence lists every value a field accepts. + /// + [Fact] + public async Task The_search_row_keeps_its_height_whatever_the_answer_says() + { + var (window, model) = await Opened(); + + var empty = Height(window); + + WpfHost.On(() => model.QueryText = "stat:runing"); + WpfHost.Settled(); + + Assert.NotEqual(string.Empty, WpfHost.On(() => model.Says.AnswerLine)); + Assert.Equal(empty, Height(window)); + + WpfHost.On(window.Close); + } + + private static double Height(MainWindow window) => WpfHost.On(() => + { + window.Search.Measure(new Size(1000, double.PositiveInfinity)); + + return window.Search.DesiredSize.Height; + }); + /// /// The LOGICAL tree rather than the visual one, which is the difference between this file and /// the six others with a helper of this name: the window here is never shown, so no template diff --git a/tests/Bws.Gui.Tests/ContrastFloors.cs b/tests/Bws.Gui.Tests/ContrastFloors.cs index 6a280eb..ae2e6f7 100644 --- a/tests/Bws.Gui.Tests/ContrastFloors.cs +++ b/tests/Bws.Gui.Tests/ContrastFloors.cs @@ -83,6 +83,15 @@ internal sealed record Floor(double Ratio, string? Against = null); // stronger of the two signals rather than competing with the furniture. ["SurfaceRowLine"] = new(1.25), + // THE LINE UNDER THE COLUMN HEADINGS, 2026-09-25, and it answers to more than the line + // between two rows on purpose: it separates the names of the columns from what is in them, + // and the owner's complaint was that the headings read as one more row. 1.84 measured. + ["SurfaceHeadingRule"] = new(1.5), + + // THE SECOND TEXT COLOUR, 2026-09-25 - the column headings. Fluent's secondary text laid on + // the window, and it is text, so it answers to WCAG's text floor: 10.15 measured. + ["TextSecondary"] = new(ForText), + // THE SCROLLBAR THUMB ASLEEP AND AWAKE, 2026-09-02, AND THE PAIR IS THE POINT. The bar was // one colour at 2.84 and the owner said it was too big and ugly - the width was half of // that and the brightness was the other half. At rest the thumb answers "where am I in the @@ -206,7 +215,7 @@ internal sealed record Floor(double Ratio, string? Against = null); /// internal static readonly string[] CarriesNoText = [ - "SurfaceRowLine", "SurfacePanelEdge", "SurfaceChipEdge", + "SurfaceRowLine", "SurfaceHeadingRule", "SurfacePanelEdge", "SurfaceChipEdge", "SurfaceScrollThumb", "SurfaceScrollThumbAwake", "SurfaceScrim" ]; } diff --git a/tests/Bws.Gui.Tests/WpfHost.cs b/tests/Bws.Gui.Tests/WpfHost.cs index 8091b11..7fda03e 100644 --- a/tests/Bws.Gui.Tests/WpfHost.cs +++ b/tests/Bws.Gui.Tests/WpfHost.cs @@ -286,7 +286,9 @@ internal static Color Declared(string name) => // Suggestions.xaml beside it, after Text.xaml whose names it resolves. // Actions.xaml after Menus.xaml, as in App.xaml - it extends the implicit Button // style the same way. The write buttons and their shield, 2026-09-23. - "Controls.xaml", "Menus.xaml", "Actions.xaml", "Suggestions.xaml", + // SearchField.xaml after Suggestions.xaml, as in App.xaml - its panel of + // messages wears SuggestionPanel. The search field, 2026-09-25. + "Controls.xaml", "Menus.xaml", "Actions.xaml", "Suggestions.xaml", "SearchField.xaml", // PlanLines.xaml before Plan.xaml, for the same reason and from the same // list in App.xaml. They were one file until 2026-09-07. PlanOffers.xaml From a123229c52038457b111d6c735e117794261b3e8 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Fri, 25 Sep 2026 09:59:11 +0200 Subject: [PATCH 3/3] Keep the plan over the search field, lighten the focus line, and count the red edge against the field - The search row stood on a layer of its own so the message panel under the field lies over the filters, and a layer outranks declaration order, so the plan was drawn under the search field and the field took clicks through the dimming. The two layers are named side by side in Values.xaml and the plan is the top one. WindowLayerGuards asks it of a hit test on a shown window. - The focus line of a text field was the selection blue, 2.51 against the field fill it stands on. FocusLine is the same hue lighter, 3.34 on the worst fill (a field under the pointer on the plan sheet). ContrastGuards composes every text box fill over the window and the sheet and holds the line to 3:1, and holds the library key to the same colour. - The red edge test asked for more than 20 red pixels, 22 here and 18 on the CI runner. It asks for half the field's height now. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +- src/Bws.Gui/MainWindow.xaml | 7 +- src/Bws.Gui/Themes/SearchField.xaml | 6 +- src/Bws.Gui/Themes/Surfaces.xaml | 33 ++++++-- src/Bws.Gui/Themes/Values.xaml | 15 ++++ tests/Bws.Gui.Tests/AnswerLineGuards.cs | 19 +++-- tests/Bws.Gui.Tests/ContrastFloors.cs | 6 ++ tests/Bws.Gui.Tests/ContrastGuards.cs | 58 +++++++++++++ tests/Bws.Gui.Tests/WindowLayerGuards.cs | 100 +++++++++++++++++++++++ 9 files changed, 230 insertions(+), 19 deletions(-) create mode 100644 tests/Bws.Gui.Tests/WindowLayerGuards.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ddac5b..af67866 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,8 +74,9 @@ is not part of this repository. - "Show the list" on the machine overview is a blue button, so it is easy to find the first time the window opens. - In the plan panel, the name of the entry stands out in bold in the title and in every step. -- The search box has a magnifier and a lighter fill, and a blue line under it while you type in - it. Before, it was darker than the window, and the line took the accent colour of your Windows. +- The search box has a magnifier and a lighter fill. Before, it was darker than the window. +- Every text box has a light blue line under it while you type in it. Before, the line took the + accent colour of your Windows. - A mistake in the query, or a note about something still being read, no longer takes a line under the search box, so the window no longer moves when you start typing. While you are in the box, a panel under it shows every message in full, over the filters. When you leave the box, the box diff --git a/src/Bws.Gui/MainWindow.xaml b/src/Bws.Gui/MainWindow.xaml index d40e5c6..5c03352 100644 --- a/src/Bws.Gui/MainWindow.xaml +++ b/src/Bws.Gui/MainWindow.xaml @@ -126,7 +126,7 @@ --> @@ -360,10 +360,13 @@ WHAT AN OPERATION OVER THE SELECTION WOULD DO - a LAYER OVER THE WHOLE WINDOW since 2026-09-02 rather than a cell beside the list, still mutually exclusive with the details panel. LAST IN THIS FILE ON PURPOSE: siblings paint in document order, so one place - higher would have put the status row over its dimming, elevate button and all. + higher would have put the status row over its dimming, elevate button and all. AND ON + THE TOP LAYER since the search row stands on one of its own - a layer outranks the + order, Values.xaml says how that was found. --> diff --git a/src/Bws.Gui/Themes/SearchField.xaml b/src/Bws.Gui/Themes/SearchField.xaml index f6db98d..9c2fc1a 100644 --- a/src/Bws.Gui/Themes/SearchField.xaml +++ b/src/Bws.Gui/Themes/SearchField.xaml @@ -68,8 +68,8 @@ @@ -80,7 +80,7 @@ - + diff --git a/src/Bws.Gui/Themes/Surfaces.xaml b/src/Bws.Gui/Themes/Surfaces.xaml index e6b7a30..86d2c84 100644 --- a/src/Bws.Gui/Themes/Surfaces.xaml +++ b/src/Bws.Gui/Themes/Surfaces.xaml @@ -347,19 +347,42 @@ a button do not read as one control. Translucent for the reason the buttons are: the box of seconds stands on the plan sheet. - THE FOCUS LINE IS THE SELECTION BLUE, the one the scope tabs are underlined with, so "this is - where you are" is one colour in this window rather than one colour per machine. + THE FOCUS LINE IS OUR OWN BLUE, not the machine's accent, so "this is where you are" is one + colour in this window rather than one colour per machine. FocusLine below says which blue. --> + + + #65A3D8 + - + diff --git a/src/Bws.Gui/Themes/Values.xaml b/src/Bws.Gui/Themes/Values.xaml index 443f1b7..c8caaf5 100644 --- a/src/Bws.Gui/Themes/Values.xaml +++ b/src/Bws.Gui/Themes/Values.xaml @@ -384,4 +384,19 @@ three lines would be 72 and still do not fit. --> 56 + + + 1 + 2 diff --git a/tests/Bws.Gui.Tests/AnswerLineGuards.cs b/tests/Bws.Gui.Tests/AnswerLineGuards.cs index e8ce49c..53c5143 100644 --- a/tests/Bws.Gui.Tests/AnswerLineGuards.cs +++ b/tests/Bws.Gui.Tests/AnswerLineGuards.cs @@ -29,23 +29,28 @@ public async Task The_search_box_wears_the_problem_colour_on_its_edge_while_its_ { var (window, model) = await Opened(); - var fine = RedInsideTheBox(window, "fine"); + var (fine, _) = RedInsideTheBox(window, "fine"); WpfHost.On(() => model.QueryText = "stat:runing"); WpfHost.Settled(); - var wrong = RedInsideTheBox(window, "wrong"); + var (wrong, tall) = RedInsideTheBox(window, "wrong"); Assert.True( WpfHost.On(() => Validation.GetHasError(window.Search.Box)), "the binding on the search box never heard that the query is wrong - INotifyDataErrorInfo " + "is not reaching Validation.HasError, so no style can colour the edge"); + // HALF THE FIELD'S HEIGHT, NOT A NUMBER - review of PR 21. The edge runs the height of the + // field less its two rounded corners, and a smoothed pixel is not counted, so what it can + // reach follows the field. It asked for more than 20 until then: 22 of a field about 30 tall on + // the machine it was written on, and 18 on the CI runner, whose picture is not kept - why + // it counted four fewer was not measured. Assert.Equal(0, fine); Assert.True( - wrong > 20, - $"only {wrong} pixels of the problem colour were painted inside the search box while it " - + "holds 'stat:runing'. The style sets BorderBrush and the library's template is not drawing it."); + wrong * 2 > tall, + $"only {wrong} pixels of the problem colour down the left edge of a field {tall} pixels " + + "tall while it holds 'stat:runing'. The frame's style sets BorderBrush and it is not reaching the pixel."); WpfHost.On(window.Close); } @@ -154,7 +159,7 @@ private static TextBlock Line(MainWindow window, string automationId) => /// sentence at the field's right end wears the same red, so a count over the whole field would /// pass on the sentence alone. Nothing but the edge is red at the left: the magnifier is grey. /// - private static int RedInsideTheBox(MainWindow window, string state) + private static (int Red, int Tall) RedInsideTheBox(MainWindow window, string state) { var drawn = Drawn.Of(window.Search, 1000, 200); var frame = drawn.Around(window.Search.SearchField); @@ -163,7 +168,7 @@ private static int RedInsideTheBox(MainWindow window, string state) drawn.Save($"search-row-{state}-1000x200.png"); - return drawn.Count(WpfHost.Declared("MeaningRejected"), new Int32Rect(frame.X, frame.Y, EdgeStrip, frame.Height)); + return (drawn.Count(WpfHost.Declared("MeaningRejected"), new Int32Rect(frame.X, frame.Y, EdgeStrip, frame.Height)), frame.Height); } /// How far in from the frame's left edge the red is counted - the edge and its antialiasing, short of the magnifier. diff --git a/tests/Bws.Gui.Tests/ContrastFloors.cs b/tests/Bws.Gui.Tests/ContrastFloors.cs index ae2e6f7..04b1392 100644 --- a/tests/Bws.Gui.Tests/ContrastFloors.cs +++ b/tests/Bws.Gui.Tests/ContrastFloors.cs @@ -167,6 +167,12 @@ internal sealed record Floor(double Ratio, string? Against = null); // worst of four surfaces and lives in its own test further down. ["FocusRing"] = new(ForState), + // THE SAME SHAPE FOR THE LINE UNDER A TEXT FIELD WITH THE KEYBOARD IN IT, review of PR 21. + // Against the window here, where it clears at 6.04. It is drawn on the field's own fill, + // which is translucent, so its real floor is every fill composed over the window and over + // the plan sheet - its own test in ContrastGuards, with the ring's. + ["FocusLine"] = new(ForState), + // THE FOUR START TYPES, 2026-08-17. SC 1.4.11 rather than the text floor, and the // distinction is not a rounding: nobody reads these, they are rings beside a word that // carries the same answer in text. What they have to do is be TELLABLE from the window and diff --git a/tests/Bws.Gui.Tests/ContrastGuards.cs b/tests/Bws.Gui.Tests/ContrastGuards.cs index db9c805..47bb787 100644 --- a/tests/Bws.Gui.Tests/ContrastGuards.cs +++ b/tests/Bws.Gui.Tests/ContrastGuards.cs @@ -181,6 +181,64 @@ public void The_focus_ring_clears_its_ratio_against_every_surface_a_row_can_take + Environment.NewLine + string.Join(Environment.NewLine, short_)); } + /// + /// The line under a text field with the keyboard in it clears 3:1 against every fill a text box + /// takes, on every surface a text box stands on - review of PR 21. + /// + /// It was the selection blue, 3.09 against the window and 2.51 against the fill it is + /// actually drawn on. The fills are translucent white, so what the line meets is the fill + /// composed over what the field stands on: the window for the search field, the plan sheet for + /// the box of seconds and the box a confirmation is typed into. Resolved out of the running theme rather + /// than read from the files, because the fills are written as attributes and + /// sees only the element form - backlog 459. + /// + /// And the library's key has to carry the same colour, since every text box but the + /// search field draws its line through it and an alias of FocusLine does not reach that template. + /// + [Fact] + public void The_focus_line_clears_its_ratio_against_every_fill_a_text_box_takes() + { + var line = WpfHost.Declared("FocusLine"); + + Assert.Equal(line, WpfHost.Declared("TextControlFocusedBorderBrush")); + + var surfaces = new[] { ("the window", WindowBackground()), ("the plan sheet", WpfHost.Declared("SurfacePanel")) }; + var short_ = new List(); + + foreach (var (surface, under) in surfaces) + { + foreach (var fill in FillsOfATextBox) + { + var beneath = Composed(WpfHost.Declared(fill), under); + var ratio = Contrast(line, beneath); + + if (ratio < ForState) + { + short_.Add(string.Create( + System.Globalization.CultureInfo.InvariantCulture, + $" {fill} on {surface} reads {Hex(beneath)}, the line measures {ratio:F2} and needs {ForState:F1}")); + } + } + } + + Assert.True( + short_.Count == 0, + "The focus line is drawn on the field's own fill, so it has to be tellable from all of them:" + + Environment.NewLine + string.Join(Environment.NewLine, short_)); + } + + /// The library's text box fills, all three of which Surfaces.xaml overrides. + private static readonly string[] FillsOfATextBox = + ["TextControlBackground", "TextControlBackgroundFocused", "TextControlBackgroundPointerOver"]; + + /// A translucent colour laid over an opaque one, channel by channel, the way it reaches the pixel. + private static Color Composed(Color over, Color under) + { + byte Channel(byte top, byte bottom) => (byte)Math.Round(((top * over.A) + (bottom * (255 - over.A))) / 255.0); + + return Color.FromRgb(Channel(over.R, under.R), Channel(over.G, under.G), Channel(over.B, under.B)); + } + [Fact] public void No_colour_is_declared_without_a_ratio_it_has_to_clear() { diff --git a/tests/Bws.Gui.Tests/WindowLayerGuards.cs b/tests/Bws.Gui.Tests/WindowLayerGuards.cs new file mode 100644 index 0000000..6b8038e --- /dev/null +++ b/tests/Bws.Gui.Tests/WindowLayerGuards.cs @@ -0,0 +1,100 @@ +using System.Windows; +using System.Windows.Media; +using Bws.Core.Planning; +using Bws.Gui.ViewModels; + +namespace Bws.Gui.Tests; + +/// +/// The layers of the window - which part of MainWindow.xaml's grid is drawn over which, asked of a +/// click on a shown window. Values.xaml names the layers and says why there are two. +/// +/// Its own file because the question is the window's, not a panel's. The plan panel and the +/// search row each have a file of guards, and neither can see the other - the fault this file +/// exists for stood between them. +/// +public sealed class WindowLayerGuards +{ + /// + /// THE PLAN LIES OVER THE WHOLE WINDOW, THE SEARCH FIELD INCLUDED - review of PR 21. + /// + /// The search row went up a layer on 2026-09-25 so the panel of messages under the field + /// lies over the filters, and a layer outranks the order siblings are declared in. The plan, + /// last in MainWindow.xaml so it would cover everything, was drawn under the search field, and + /// the field took clicks and keys through the dimming while a plan waited to be carried out. + /// + /// Asked with the framework's own hit test at the centre of the search field on a shown + /// window, the way a click asks it. Hit testing walks the layers in the order painting does, so + /// the one answer covers what is seen as well as what is clicked. + /// + [Fact] + public async Task The_plan_lies_over_the_whole_window_the_search_field_included() + { + var window = await WithOnePicked(); + + Assert.True(await WpfHost.On(() => window.Preview(ActionKind.Stop))); + WpfHost.Settled(); + + var (hit, onThePlan) = WpfHost.On(() => + { + // SHOWN, off screen, the way PlanViewGuards shows the panel it reads colours from: hit + // testing needs a laid out window, and a window built here is never laid out otherwise. + window.Width = 1100; + window.Height = 700; + window.WindowStyle = WindowStyle.None; + window.ShowInTaskbar = false; + window.Left = -4000; + window.Show(); + window.UpdateLayout(); + + var field = window.Search.SearchField; + var centre = field.TranslatePoint(new Point(field.ActualWidth / 2, field.ActualHeight / 2), window); + var found = window.InputHitTest(centre) as DependencyObject; + + return (found?.GetType().Name ?? "nothing", IsInside(found, window.PlanPanel)); + }); + + Assert.True( + onThePlan, + $"a click at the centre of the search field lands on {hit} while a plan is open, not on " + + "the plan's dimming - the search row is drawn over the plan and can be typed into behind it"); + + WpfHost.On(window.Close); + } + + private static bool IsInside(DependencyObject? hit, DependencyObject target) + { + for (var at = hit; at is not null; at = at is Visual ? VisualTreeHelper.GetParent(at) : LogicalTreeHelper.GetParent(at)) + { + if (ReferenceEquals(at, target)) + { + return true; + } + } + + return false; + } + + /// + /// A window with one entry picked, so there is a plan to open. Read before the model reaches + /// the window, the order PlanViewGuards keeps and for its reason. + /// + private static async Task WithOnePicked() + { + var model = new MainViewModel(new LiveMachine(Rows.Entry("Spooler", "Print Spooler")), new SteppedClock()); + + await model.LoadAsync(); + + var window = WpfHost.Window(model); + + WpfHost.On(() => + { + window.Entries.ItemsSource = model.Rows; + window.Entries.SelectedItem = model.Rows.First(row => row.ServiceName == "Spooler"); + }); + + WpfHost.Settled(); + + return window; + } +}