Skip to content

Commit 2740184

Browse files
jhaygood86claude
authored andcommitted
Evaluate @media against the platform, via two new adapter properties
@media conditions were never really evaluated: DomParser cascaded against a hard-coded media type of "all" and CssData compared only the medium's type, so every `@media screen` / `@media print` block was dropped, a feature-only query like `@media (prefers-color-scheme: dark)` was dropped, and `@media all and (prefers-color-scheme: dark)` matched unconditionally - applying rules guarded by a condition nothing had tested. RAdapter gains two virtuals describing the surface being rendered onto: - DefaultMediaType - "screen" by default, overridden to "print" in PdfSharpAdapter - SystemColorScheme - Light by default; WinForms and WPF read the Windows app theme (HKCU\...\Themes\Personalize\AppsUseLightTheme), cached and invalidated on SystemEvents.UserPreferenceChanged rather than read per query MediaQueryContext captures that device - media type, viewport size in CSS px, resolution and colour scheme - and MediaQueryMatcher evaluates a rule's enclosing @media chain against it: width/height, orientation, aspect-ratio, resolution, colour depth, and the prefers-* set. Its central policy is Media Queries 4's "unknown feature is false": a feature the matcher cannot evaluate drops its whole block rather than letting the rules inside apply untested. A feature it does model but whose context is missing (a width query before the viewport is known) stays permissive - that is an unknown context, not an unsupported feature. This is a deliberate behaviour change: @media screen blocks now apply on WinForms/WPF and @media print blocks on PdfSharp, where previously neither did. Also fixes colour resolution inside a function: the render-time tokenizer ran outside value mode, where a digit-leading hex splits into a "#" delimiter plus a dimension, so color-mix(in srgb, #ffffff 0%, #0000ff) failed to resolve while the same value with named operands worked. TestSamples/23 exercises all of it in the WinForms and WPF demos: a card that follows the OS theme, one card per media type, a card proving an unevaluable feature does not apply, and a viewport-width probe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 36b3170 commit 2740184

14 files changed

Lines changed: 649 additions & 49 deletions

File tree

‎Source/Demo/Common/HtmlRenderer.Demo.Common.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
<EmbeddedResource Include="TestSamples\20.Fonts decorations.htm" />
6767
<EmbeddedResource Include="TestSamples\21.Bullets.htm" />
6868
<EmbeddedResource Include="TestSamples\22.RTL.htm" />
69+
<EmbeddedResource Include="TestSamples\23.Media queries and color scheme.htm" />
6970
<EmbeddedResource Include="TestSamples\30.Misc.htm" />
7071
<EmbeddedResource Include="TestSamples\31.ACID 1.htm" />
7172
<EmbeddedResource Include="TestSamples\32.Image in css content.htm" />
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<html>
2+
<head>
3+
<style>
4+
body { font: 10pt Tahoma; margin: 10px; }
5+
6+
.card {
7+
border: 1px solid #767676;
8+
padding: 10px;
9+
margin-bottom: 12px;
10+
}
11+
12+
h3 { margin: 0 0 6px 0; font-size: 12pt; }
13+
.note { color: #555555; font-size: 8pt; }
14+
15+
/* --- prefers-color-scheme -------------------------------------------------
16+
The light styling below is the baseline. On GDI+ and WPF the adapter reports
17+
the Windows app theme, so switching Windows to Dark and reloading should
18+
flip this card to the dark palette. PdfSharp always reports light. */
19+
.scheme {
20+
background-color: #ffffff;
21+
color: #111111;
22+
border-color: #cccccc;
23+
}
24+
.scheme .verdict:after { content: "light"; }
25+
26+
@media (prefers-color-scheme: dark) {
27+
.scheme {
28+
background-color: #1e1e1e;
29+
color: #f0f0f0;
30+
border-color: #555555;
31+
}
32+
.scheme .verdict:after { content: "dark"; }
33+
}
34+
35+
/* --- media type ----------------------------------------------------------
36+
The adapter also supplies the media type: "screen" for GDI+/WPF, "print"
37+
for PdfSharp. Exactly one of these two cards should be visible. */
38+
.screen-only { display: none; }
39+
.print-only { display: none; }
40+
41+
@media screen {
42+
.screen-only { display: block; background-color: #e8f4ff; }
43+
}
44+
45+
@media print {
46+
.print-only { display: block; background-color: #f4f4f4; }
47+
}
48+
49+
/* --- feature evaluation --------------------------------------------------
50+
A feature the matcher cannot evaluate drops its whole block, so this card
51+
must never turn red. */
52+
@media all and (scan: interlace) {
53+
.unsupported-feature { background-color: #ff0000; color: #ffffff; }
54+
}
55+
56+
/* --- viewport width ------------------------------------------------------
57+
Evaluated against the container's available width. */
58+
.width-probe { background-color: #f0f0f0; }
59+
@media (min-width: 400px) {
60+
.width-probe { background-color: #e6ffe6; }
61+
.width-probe .verdict:after { content: "viewport is at least 400px wide"; }
62+
}
63+
@media (max-width: 399px) {
64+
.width-probe .verdict:after { content: "viewport is under 400px wide"; }
65+
}
66+
</style>
67+
</head>
68+
<body>
69+
70+
<div class="card scheme">
71+
<h3>prefers-color-scheme</h3>
72+
This card follows the platform colour scheme. Reported scheme: <b class="verdict"></b>.
73+
<div class="note">GDI+ and WPF read the Windows app theme; PdfSharp always reports light.</div>
74+
</div>
75+
76+
<div class="card screen-only">
77+
<h3>@media screen</h3>
78+
Visible because the adapter's media type is <b>screen</b>.
79+
</div>
80+
81+
<div class="card print-only">
82+
<h3>@media print</h3>
83+
Visible because the adapter's media type is <b>print</b>.
84+
</div>
85+
86+
<div class="card unsupported-feature">
87+
<h3>Unsupported media feature</h3>
88+
This card stays neutral. A media feature the matcher cannot evaluate drops its whole block
89+
rather than applying the rules inside it, so the red styling in
90+
<code>@media all and (scan: interlace)</code> must not win.
91+
</div>
92+
93+
<div class="card width-probe">
94+
<h3>Viewport width</h3>
95+
<span class="verdict"></span>
96+
</div>
97+
98+
</body>
99+
</html>

‎Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ public static PdfSharpAdapter Instance
6767
get { return _instance; }
6868
}
6969

70+
/// <summary>
71+
/// Paged output, so @media print applies and @media screen does not.
72+
/// </summary>
73+
public override string DefaultMediaType
74+
{
75+
get { return "print"; }
76+
}
77+
78+
/// <summary>
79+
/// A PDF has no system theme to follow, so prefers-color-scheme always reports light.
80+
/// </summary>
81+
public override RColorScheme SystemColorScheme
82+
{
83+
get { return RColorScheme.Light; }
84+
}
85+
7086
/// <summary>
7187
/// Get the FontResolver instance for advanced font management.
7288
/// </summary>

‎Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ private WpfAdapter()
7171
{
7272
}
7373
}
74+
75+
SystemEvents.UserPreferenceChanged += (sender, e) =>
76+
{
77+
if (e.Category == UserPreferenceCategory.General)
78+
_colorScheme = null;
79+
};
7480
}
7581

7682
/// <summary>
@@ -81,6 +87,25 @@ public static WpfAdapter Instance
8187
get { return _instance; }
8288
}
8389

90+
/// <summary>
91+
/// Rendering onto a Windows surface, so the document should follow the user's app theme.
92+
/// Cached and invalidated on a system preference change rather than read per query.
93+
/// </summary>
94+
public override RColorScheme SystemColorScheme
95+
{
96+
get
97+
{
98+
if (!_colorScheme.HasValue)
99+
_colorScheme = WindowsTheme.GetAppsColorScheme();
100+
return _colorScheme.Value;
101+
}
102+
}
103+
104+
/// <summary>
105+
/// Cached app theme; null when it needs to be re-read.
106+
/// </summary>
107+
private RColorScheme? _colorScheme;
108+
84109
protected override RColor GetColorInt(string colorName)
85110
{
86111
// check if color name is valid to avoid ColorConverter throwing an exception
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// "Therefore those skilled at the unorthodox
2+
// are infinite as heaven and earth,
3+
// inexhaustible as the great rivers.
4+
// When they come to an end,
5+
// they begin again,
6+
// like the days and months;
7+
// they die and are reborn,
8+
// like the four seasons."
9+
//
10+
// - Sun Tsu,
11+
// "The Art of War"
12+
13+
using Microsoft.Win32;
14+
using TheArtOfDev.HtmlRenderer.Adapters;
15+
16+
namespace TheArtOfDev.HtmlRenderer.WPF.Utilities
17+
{
18+
/// <summary>
19+
/// Reads the Windows app theme, which is what <c>prefers-color-scheme</c> reports for on-screen
20+
/// rendering.
21+
/// </summary>
22+
internal static class WindowsTheme
23+
{
24+
private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
25+
private const string AppsUseLightTheme = "AppsUseLightTheme";
26+
27+
/// <summary>
28+
/// The user's app theme, or <see cref="RColorScheme.Light"/> when it cannot be determined -
29+
/// light is the Windows default and the safer assumption for a document that declares no dark
30+
/// styling of its own.
31+
/// </summary>
32+
public static RColorScheme GetAppsColorScheme()
33+
{
34+
try
35+
{
36+
using (var key = Registry.CurrentUser.OpenSubKey(PersonalizeKey))
37+
{
38+
if (key != null)
39+
{
40+
var value = key.GetValue(AppsUseLightTheme);
41+
if (value is int)
42+
return (int)value == 0 ? RColorScheme.Dark : RColorScheme.Light;
43+
}
44+
}
45+
}
46+
catch
47+
{
48+
// A locked-down or missing key just means "no preference expressed".
49+
}
50+
51+
return RColorScheme.Light;
52+
}
53+
}
54+
}

‎Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ private WinFormsAdapter()
4848
{
4949
AddFontFamily(new FontFamilyAdapter(family));
5050
}
51+
52+
Microsoft.Win32.SystemEvents.UserPreferenceChanged += (sender, e) =>
53+
{
54+
if (e.Category == Microsoft.Win32.UserPreferenceCategory.General)
55+
_colorScheme = null;
56+
};
5157
}
5258

5359
/// <summary>
@@ -58,6 +64,26 @@ public static WinFormsAdapter Instance
5864
get { return _instance; }
5965
}
6066

67+
/// <summary>
68+
/// Rendering onto a Windows control, so the document should follow the user's app theme.
69+
/// Cached and invalidated on a system preference change rather than read per query.
70+
/// </summary>
71+
public override RColorScheme SystemColorScheme
72+
{
73+
get
74+
{
75+
if (!_colorScheme.HasValue)
76+
_colorScheme = WindowsTheme.GetAppsColorScheme();
77+
return _colorScheme.Value;
78+
}
79+
}
80+
81+
/// <summary>
82+
/// Cached app theme; null when it needs to be re-read.
83+
/// </summary>
84+
private RColorScheme? _colorScheme;
85+
86+
6187
protected override RColor GetColorInt(string colorName)
6288
{
6389
var color = Color.FromName(colorName);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// "Therefore those skilled at the unorthodox
2+
// are infinite as heaven and earth,
3+
// inexhaustible as the great rivers.
4+
// When they come to an end,
5+
// they begin again,
6+
// like the days and months;
7+
// they die and are reborn,
8+
// like the four seasons."
9+
//
10+
// - Sun Tsu,
11+
// "The Art of War"
12+
13+
using Microsoft.Win32;
14+
using TheArtOfDev.HtmlRenderer.Adapters;
15+
16+
namespace TheArtOfDev.HtmlRenderer.WinForms.Utilities
17+
{
18+
/// <summary>
19+
/// Reads the Windows app theme, which is what <c>prefers-color-scheme</c> reports for on-screen
20+
/// rendering.
21+
/// </summary>
22+
internal static class WindowsTheme
23+
{
24+
private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
25+
private const string AppsUseLightTheme = "AppsUseLightTheme";
26+
27+
/// <summary>
28+
/// The user's app theme, or <see cref="RColorScheme.Light"/> when it cannot be determined -
29+
/// light is the Windows default and the safer assumption for a document that declares no dark
30+
/// styling of its own.
31+
/// </summary>
32+
public static RColorScheme GetAppsColorScheme()
33+
{
34+
try
35+
{
36+
using (var key = Registry.CurrentUser.OpenSubKey(PersonalizeKey))
37+
{
38+
if (key != null)
39+
{
40+
var value = key.GetValue(AppsUseLightTheme);
41+
if (value is int)
42+
return (int)value == 0 ? RColorScheme.Dark : RColorScheme.Light;
43+
}
44+
}
45+
}
46+
catch
47+
{
48+
// A locked-down or missing key just means "no preference expressed".
49+
}
50+
51+
return RColorScheme.Light;
52+
}
53+
}
54+
}

‎Source/HtmlRenderer/Adapters/RAdapter.cs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,25 @@ protected RAdapter()
8080
_fontsHandler = new FontsHandler(this);
8181
}
8282

83+
/// <summary>
84+
/// The CSS media type this adapter renders for, used to evaluate <c>@media</c> queries -
85+
/// <c>"screen"</c> for on-screen adapters (the default), <c>"print"</c> for paged output.
86+
/// </summary>
87+
public virtual string DefaultMediaType
88+
{
89+
get { return "screen"; }
90+
}
91+
92+
/// <summary>
93+
/// The colour scheme the rendering surface presents, answering the <c>prefers-color-scheme</c>
94+
/// media feature. Defaults to <see cref="RColorScheme.Light"/>; an adapter that renders onto a
95+
/// themed surface should report the system setting instead.
96+
/// </summary>
97+
public virtual RColorScheme SystemColorScheme
98+
{
99+
get { return RColorScheme.Light; }
100+
}
101+
83102
/// <summary>
84103
/// Get the default CSS stylesheet data.
85104
/// </summary>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// "Therefore those skilled at the unorthodox
2+
// are infinite as heaven and earth,
3+
// inexhaustible as the great rivers.
4+
// When they come to an end,
5+
// they begin again,
6+
// like the days and months;
7+
// they die and are reborn,
8+
// like the four seasons."
9+
//
10+
// - Sun Tsu,
11+
// "The Art of War"
12+
13+
namespace TheArtOfDev.HtmlRenderer.Adapters
14+
{
15+
/// <summary>
16+
/// The colour scheme the rendering surface presents, as reported by the platform adapter and
17+
/// queried by the <c>prefers-color-scheme</c> media feature (Media Queries 5 §5.1).
18+
/// </summary>
19+
public enum RColorScheme
20+
{
21+
/// <summary>
22+
/// A light surface - dark content on a light background. The default for any adapter that has
23+
/// no system theme to follow.
24+
/// </summary>
25+
Light,
26+
27+
/// <summary>
28+
/// A dark surface - light content on a dark background.
29+
/// </summary>
30+
Dark
31+
}
32+
}

0 commit comments

Comments
 (0)