Skip to content

Commit 92b56ef

Browse files
jhaygood86claude
authored andcommitted
Re-evaluate @media when the viewport changes
The media context was built once, while generating the CSS tree, so a resize never re-ran the cascade: a `@media (min-width: …)` rule picked at load time stayed picked no matter how the window was resized afterwards. HtmlContainerInt now keeps the html and base stylesheet it was given, and PerformLayout re-runs the cascade when the current viewport would evaluate a @media condition differently than the one the cascade was computed against. The test is deliberately narrow, since PerformLayout runs on every layout pass. CssData collects the distinct @media chains its rules are nested under while indexing (one entry per @media block), and MediaOutcomeChanged evaluates just those against the old and new device. A document with no viewport-dependent @media - the common case - costs one empty loop, and a resize that stays within the same breakpoint costs a handful of comparisons and rebuilds nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 2740184 commit 92b56ef

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

‎Source/HtmlRenderer/Core/CssData.cs‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,62 @@ public IndexedRule(IStyleRule rule, bool isUserAgent, int documentOrder, MediaLi
124124
public MediaList[] EnclosingMedia { get; }
125125
}
126126

127+
private List<MediaList[]> _mediaConditions;
127128
private Dictionary<string, List<IndexedRule>> _tagIndex;
128129
private Dictionary<string, List<IndexedRule>> _classIndex;
129130
private Dictionary<string, List<IndexedRule>> _idIndex;
130131
private List<IndexedRule> _universalRules;
131132

133+
/// <summary>
134+
/// The distinct <c>@media</c> chains any indexed rule is nested under. Small (one entry per
135+
/// distinct <c>@media</c> block), and only used to test whether a change of device would change
136+
/// which rules apply - see <see cref="MediaOutcomeChanged"/>.
137+
/// </summary>
138+
private static List<MediaList[]> CollectMediaConditions(
139+
Dictionary<string, List<IndexedRule>> tagIndex,
140+
Dictionary<string, List<IndexedRule>> classIndex,
141+
Dictionary<string, List<IndexedRule>> idIndex,
142+
List<IndexedRule> universal)
143+
{
144+
var conditions = new List<MediaList[]>();
145+
146+
Action<IEnumerable<IndexedRule>> collect = rules =>
147+
{
148+
foreach (var indexed in rules)
149+
{
150+
if (indexed.EnclosingMedia == null) continue;
151+
if (conditions.Contains(indexed.EnclosingMedia)) continue; // reference compare: one array per block
152+
conditions.Add(indexed.EnclosingMedia);
153+
}
154+
};
155+
156+
foreach (var bucket in tagIndex.Values) collect(bucket);
157+
foreach (var bucket in classIndex.Values) collect(bucket);
158+
foreach (var bucket in idIndex.Values) collect(bucket);
159+
collect(universal);
160+
161+
return conditions;
162+
}
163+
164+
/// <summary>
165+
/// Whether moving from <paramref name="before"/> to <paramref name="after"/> changes the outcome
166+
/// of any <c>@media</c> condition in these stylesheets - i.e. whether the cascade needs to be
167+
/// re-run. False for a resize that doesn't cross any breakpoint, and for documents with no
168+
/// viewport-dependent <c>@media</c> at all (the common case), so the caller can skip the work.
169+
/// </summary>
170+
internal bool MediaOutcomeChanged(MediaQueryContext before, MediaQueryContext after)
171+
{
172+
EnsureIndex();
173+
174+
foreach (var condition in _mediaConditions)
175+
{
176+
if (MediaQueryMatcher.Matches(condition, before) != MediaQueryMatcher.Matches(condition, after))
177+
return true;
178+
}
179+
180+
return false;
181+
}
182+
132183
/// <summary>
133184
/// Every style rule in a rule list, in document order, descending into <c>@media</c> and
134185
/// <c>@layer</c> blocks and into CSS-Nesting child rules. <see cref="Stylesheet.StyleRules"/>
@@ -180,6 +231,7 @@ private void EnsureIndex()
180231
_tagIndex = tagIndex;
181232
_classIndex = classIndex;
182233
_idIndex = idIndex;
234+
_mediaConditions = CollectMediaConditions(tagIndex, classIndex, idIndex, universal);
183235
_universalRules = universal;
184236
}
185237

‎Source/HtmlRenderer/Core/HtmlContainerInt.cs‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ public sealed class HtmlContainerInt : IDisposable
126126
/// </summary>
127127
private CssData _cssData;
128128

129+
/// <summary>
130+
/// The html and base stylesheet last given to <see cref="SetHtml"/>, kept so the cascade can be
131+
/// re-run when a viewport change alters which <c>@media</c> rules apply.
132+
/// </summary>
133+
private string _htmlSource;
134+
135+
private CssData _baseCssData;
136+
137+
/// <summary>
138+
/// The device the current cascade was computed against.
139+
/// </summary>
140+
private MediaQueryContext _cascadeMedia;
141+
129142
/// <summary>
130143
/// Is content selection is enabled for the rendered html (default - true).<br/>
131144
/// If set to 'false' the rendered html will be static only with ability to click on links.
@@ -507,13 +520,17 @@ internal RColor SelectionBackColor
507520
public void SetHtml(string htmlSource, CssData baseCssData = null)
508521
{
509522
Clear();
523+
_htmlSource = htmlSource;
524+
_baseCssData = baseCssData;
525+
510526
if (!string.IsNullOrEmpty(htmlSource))
511527
{
512528
_loadComplete = false;
513529
_cssData = baseCssData ?? _adapter.DefaultCssData;
514530

515531
DomParser parser = new DomParser(_cssParser);
516532
_root = parser.GenerateCssTree(htmlSource, this, ref _cssData);
533+
_cascadeMedia = MediaQueryContext.FromAdapter(_adapter, _maxSize.Width, _maxSize.Height);
517534
if (_root != null)
518535
{
519536
_selectionHandler = new SelectionHandler(_root);
@@ -522,6 +539,23 @@ public void SetHtml(string htmlSource, CssData baseCssData = null)
522539
}
523540
}
524541

542+
/// <summary>
543+
/// Re-runs the cascade if the viewport has changed in a way that changes which <c>@media</c>
544+
/// rules apply - a resize crossing a <c>min-width</c> breakpoint, say. Nothing happens for a
545+
/// document whose stylesheets have no viewport-dependent <c>@media</c> (the common case), or for
546+
/// a resize that stays within the same breakpoint, so this costs one cheap condition test per
547+
/// layout rather than a re-parse.
548+
/// </summary>
549+
private void RefreshMediaDependentCascade()
550+
{
551+
if (_root == null || string.IsNullOrEmpty(_htmlSource) || _cssData == null) return;
552+
553+
var media = MediaQueryContext.FromAdapter(_adapter, _maxSize.Width, _maxSize.Height);
554+
if (!_cssData.MediaOutcomeChanged(_cascadeMedia, media)) return;
555+
556+
SetHtml(_htmlSource, _baseCssData);
557+
}
558+
525559
/// <summary>
526560
/// Clear the content of the HTML container releasing any resources used to render previously existing content.
527561
/// </summary>
@@ -632,6 +666,8 @@ public void PerformLayout(RGraphics g)
632666
{
633667
ArgChecker.AssertArgNotNull(g, "g");
634668

669+
RefreshMediaDependentCascade();
670+
635671
_actualSize = RSize.Empty;
636672
if (_root != null)
637673
{

0 commit comments

Comments
 (0)