fix(xaml): the app refused to launch — RecordPillToggle was defined before SegmentToggle
`BasedOn="{StaticResource SegmentToggle}"` resolves while the dictionary is being built, so a
forward reference fails at load with "Cannot find resource named 'SegmentToggle'". The style now
sits after SegmentToggle and SegmentLabel, where its base already exists.
Guard: ThemeResourceTests gains the ordering rule — within one dictionary, no StaticResource may
point at a key defined later in the same file. The existence check alone passed this, which is
exactly why the app died on launch while the tests were green.
Root cause of the unreliable test results this session: a stale `testhost` held ytLive.dll locked,
so builds silently kept the previous assembly (MSB3027 on copy). Killed it; the suite now rebuilds
and the two new facts actually run.
This commit is contained in:
+26
-25
@@ -654,31 +654,6 @@
|
||||
<Setter Property="Margin" Value="8,0,0,0"/>
|
||||
</Style>
|
||||
|
||||
<!-- REC / ON-AIR segment pills: an armed session control reads red, because red is what
|
||||
"live or recording" means (DESIGN.md). Kept separate from SegmentToggle so the Rect|Circle
|
||||
choice in the properties panel stays the interactive blue. -->
|
||||
<Style x:Key="RecordPillToggle" TargetType="ToggleButton" BasedOn="{StaticResource SegmentToggle}">
|
||||
<!-- SegmentToggle's template hard-codes a Transparent face and paints the checked state
|
||||
itself, so an armed pill needs its own template that honours Background. -->
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border x:Name="Seg" Background="{TemplateBinding Background}" CornerRadius="3"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource Brush.Primary}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource Brush.OnError}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- Sliding pill toggle: OFF = neutral, ON = blue. Red is reserved for live/record
|
||||
(DESIGN.md), so a settings toggle can never use it. -->
|
||||
<Style x:Key="PillToggle" TargetType="ToggleButton">
|
||||
@@ -775,6 +750,32 @@
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- REC / ON-AIR segment pills: an armed session control reads red, because red is what
|
||||
"live or recording" means (DESIGN.md). Kept separate from SegmentToggle so the Rect|Circle
|
||||
choice in the properties panel stays the interactive blue. -->
|
||||
<Style x:Key="RecordPillToggle" TargetType="ToggleButton" BasedOn="{StaticResource SegmentToggle}">
|
||||
<!-- SegmentToggle's template hard-codes a Transparent face and paints the checked state
|
||||
itself, so an armed pill needs its own template that honours Background. -->
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border x:Name="Seg" Background="{TemplateBinding Background}" CornerRadius="3"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource Brush.Primary}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource Brush.OnError}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
|
||||
<!-- Value-to-visibility converters, promoted from MainWindow.Window.Resources
|
||||
so extracted UserControls can resolve them (AGENTS: controls must not
|
||||
reach into a window's own resources; they share the app-level theme). -->
|
||||
|
||||
@@ -46,6 +46,36 @@ public class ThemeResourceTests
|
||||
+ string.Join("\n ", missing.Distinct()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_dictionary_never_references_a_key_defined_later_in_itself()
|
||||
{
|
||||
// StaticResource resolves while the dictionary is being built, so a forward reference
|
||||
// fails at load with "Cannot find resource named …" — exactly how the app refused to
|
||||
// launch on 2026-10-30 (a BasedOn pointing at a style defined further down the file).
|
||||
var root = System.IO.Path.GetDirectoryName(SourceAssert.RepoPath("App.xaml"))!;
|
||||
var files = new[] { System.IO.Path.Combine(root, "App.xaml") }
|
||||
.Concat(Directory.GetFiles(System.IO.Path.Combine(root, "Themes"), "*.xaml"));
|
||||
|
||||
var forward = new List<string>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
var text = File.ReadAllText(file);
|
||||
var definitions = Regex.Matches(text, @"x:Key=""([^""]+)""")
|
||||
.ToDictionary(m => m.Groups[1].Value, m => m.Index, StringComparer.Ordinal);
|
||||
|
||||
foreach (Match use in Regex.Matches(text, @"\{StaticResource ([^}]+)\}"))
|
||||
{
|
||||
var key = use.Groups[1].Value.Trim();
|
||||
if (!definitions.TryGetValue(key, out var defined)) continue;
|
||||
if (defined > use.Index)
|
||||
forward.Add($"{System.IO.Path.GetFileName(file)}: {key} used before it is defined");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(forward.Count == 0,
|
||||
"Forward StaticResource references fail at load:\n " + string.Join("\n ", forward.Distinct()));
|
||||
}
|
||||
|
||||
private static HashSet<string> KeysIn(string text)
|
||||
=> Regex.Matches(text, @"x:Key=""([^""]+)""").Select(m => m.Groups[1].Value)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
Reference in New Issue
Block a user