diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb29594..af67866 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -74,6 +74,18 @@ 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. 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
+ 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 +95,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/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/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.
-->
+
+
@@ -359,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/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..9c2fc1a
--- /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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #65A3D8
+
+
+
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
@@ -365,4 +384,19 @@
three lines would be 72 and still do not fit.
-->
56
+
+
+ 1
+ 2
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..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);
}
@@ -147,21 +152,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)
+ private static (int Red, int Tall) 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)), 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..04b1392 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
@@ -158,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
@@ -206,7 +221,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/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/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)
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;
+ }
+}
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