Keep one set of computed styles current while the pointer moves, focus changes and a user ticks a checkbox, restyle only what each change reaches, and read the styles of a range slider’s thumb and a field’s placeholder.
DocumentStyles.WithContext answers one what-if question at a time. A renderer that moves the pointer every frame needs styles that move in place instead. LiveDocumentStyles (1.3.0 and later) does that: it owns its buffers, restyles only the elements a change reaches, and allocates nothing once warmed up. Form state from script and the range slider parts need 1.4.0 and 1.5.0.
The page is a settings form, settings.html, with this stylesheet:
body { font-family: Georgia, serif; color: #222 } button { background-color: #eee; color: #222 } button:hover { background-color: #2f6b4f; color: #fff } button:focus-visible { outline: 2px solid #b8432f } input:checked + label { font-weight: 700 } fieldset:disabled label { color: #767676 } input[type=range]::-webkit-slider-thumb { background-color: #999 } input[type=range]:hover::-webkit-slider-thumb { background-color: #b8432f } input::placeholder { color: #6b6b6b; font-style: italic }
using RecursiveAI.OneHtml; using RecursiveAI.OneHtml.Css; using RecursiveAI.OneHtml.Selectors; Document doc = Document.Parse(File.ReadAllBytes("settings.html")); DocumentStyles styles = DocumentStyles.Create(doc); // A live view owns its buffers and moves in place. // The snapshot it was made from is untouched. LiveDocumentStyles live = LiveDocumentStyles.Create(styles); Node button = doc.Root.QuerySelector("button"); Node slider = doc.Root.QuerySelector("#size"); Node due = doc.Root.QuerySelector("#due"); Node dueLabel = doc.Root.QuerySelector("label[for=due]"); Node fieldset = doc.Root.QuerySelector("#alerts"); Node holdsLabel = doc.Root.QuerySelector("label[for=holds]"); string Css(Node element, string property) => live.GetStyle(element).GetPropertyValue(property); string Label(Node label) => $"font-weight {Css(label, "font-weight")}, color {Css(label, "color")}"; void Report(string step) => Console.WriteLine($"{step}: {live.ChangedElementCount} restyled"); // The pointer moves onto the button, then to the slider. live.ApplyState(ElementStateChange.Hover(button)); Report("hover button"); Console.WriteLine($" button background: {Css(button, "background-color")}"); live.ApplyState(ElementStateChange.Hover(slider)); Report("hover slider"); Console.WriteLine($" button background: {Css(button, "background-color")}"); ComputedStyle thumb = live.GetPseudoStyle(slider, PseudoElementKind.SliderThumb)!; Console.WriteLine($" slider thumb: {thumb.GetPropertyValue("background-color")}"); // Keyboard focus on the button; the hover stays where it is. live.ApplyState(ElementStateChange.Focus(button, FocusSource.Keyboard)); Report("focus button"); Console.WriteLine($" button outline: {Css(button, "outline-style")} " + $"{Css(button, "outline-color")}"); // A user ticks a checkbox and a script disables the fieldset. Neither is in the HTML. live.ApplyFormState([ FormStateChange.Checked(due, true), FormStateChange.Disabled(fieldset, true), ]); Report("form state"); Console.WriteLine($" due label: {Label(dueLabel)}"); Console.WriteLine($" holds label: {Label(holdsLabel)}"); // Generated boxes: the search field's placeholder. Node search = doc.Root.QuerySelector("#q"); ComputedStyle placeholder = live.GetPseudoStyle(search, PseudoElementKind.Placeholder)!; Console.WriteLine($"placeholder: {placeholder.GetPropertyValue("color")}, " + $"{placeholder.GetPropertyValue("font-style")}"); // The current state as an immutable snapshot, and the snapshot the view started from. string Weight(DocumentStyles s) => s.GetStyle(dueLabel).GetPropertyValue("font-weight"); Console.WriteLine($"due label now: {Weight(live.ToSnapshot())}, " + $"at the start: {Weight(styles)}");
hover button: 1 restyled button background: #2f6b4f hover slider: 1 restyled button background: #eee slider thumb: #b8432f focus button: 1 restyled button outline: solid #b8432f form state: 2 restyled due label: font-weight 700, color #767676 holds label: font-weight normal, color #767676 placeholder: #6b6b6b, italic due label now: 700, at the start: normal
How to read it:
ApplyStatetakes anElementStateChange:Hover,Active,Focus(with itsFocusSource) andTarget, alone or combined withWithHoverand the like. The parts it names move and the others keep their value, so the focus step leaves the slider hovered.ChangedElementIndiceslists the elements whose style object changed, valid until the next change;ChangedElementsInto(Span<int>)copies them into your own buffer. Hovering the slider restyles one element, the button that lost its hover. The thumb is a generated box, read withGetPseudoStyle, so it isn’t in the list.HasInteractivePseudoElementRulestells you when generated boxes can change with state, as they do here.ApplyFormStatestates what isn’t in the attributes: a checkbox the user ticked, a control a script disabled, a checkbox set to indeterminate.:checked,:disabled,:enabled,:indeterminate,:read-onlyand:read-writefollow it, with what follows from it: the disabled fieldset greys both labels. Passnullas the value to read the attributes again. On the snapshot path, the same overrides go inStyleContext.WithFormState.PseudoElementKindcoversBefore,After,Marker,Selection,Placeholder,SliderThumb,SliderTrackandSliderFill. The slider parts answer to the standard names and to the-webkit-and-moz-ones. A state pseudo-class on the part itself, such as::-webkit-slider-thumb:hover, isn’t supported.ToSnapshot()gives an immutableDocumentStylesfor the current state, equal toDocumentStyles.Createwith the contextGetContext()returns. The snapshot the live view was made from never changes.- One thread writes: don’t call
ApplyStatebeside another call on the same object. Between changes any number of threads can read, and aComputedStyleyou were handed stays valid.
Media features and the viewport are fixed when the live view is made. For another viewport, make new styles with WithContext and a new live view. The accessibility tree ignores form-state overrides, so build it from a document whose attributes say what the page shows. The full rules are in Live interaction state and Form state from script. Compute CSS styles covers the snapshot API.