mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-08-01 00:08:05 -04:00
ADDED css min(), max() and clamp() for lengths css-wide keywords inherit, initial, unset, revert (incl. shorthands) added currentColor keyword to css colors can parse oklch(), lab() and hwb() added css text-align: justify css white-space: pre-wrap and break-spaces added css word-break: break-word parses css image-rendering: crisp-edges (point sampling) added css inset shorthand now parses 1–4 values in the css border-width shorthand added css transition-duration, -delay, -property and -timing-function longhands css :has() now supports descendant selectors added css opacity percentages added css overflow: auto (maps to scroll) added css gap: normal added css filter: none css transform: scale() now takes comma-separated args css animations now accept ms units added more css justify-content, align-* and text-align keywords (start, end, …) parses css dvh/svh/lvh/dvw viewport units (treated as vh/vw) parses css object-fit: scale-down (treated as contain) added css margin-block / margin-inline (+ the logical margin sides) added css padding-block / padding-inline (+ the logical padding sides) added css inset-block / inset-inline (+ the logical inset sides) added the css flex-flow shorthand added the css font shorthand added css font-size keywords (xx-small … xxx-large) css letter-spacing and word-spacing now accept normal added css font-smooth: none css aspect-ratio now accepts the auto form css font-family now maps generic families (serif, sans-serif, monospace) added css flex-flow added css font FIXED fixed css !important dropping the whole declaration css line-height without units is now a multiplier fixed css calc() division and full-expression parsing fixed the css flex shorthand for single-number and three-value forms fixed the css transition shorthand when the property is omitted css font-family now uses the first family in a comma stack colors now parse fully in the css background shorthand an unknown css @-rule no longer drops the whole stylesheet css variables no longer match on partial tokens injected css variables are kept across stylesheet hotloads fixed css selector specificity overflowing when comparing rules css background: none now resets the background (image and colour) css filter: none now overrides a filter set by a base class css transform: none now overrides a transform set by a base class IMPROVED css rule matching is ~3–5× faster on large stylesheets (rules indexed by class) ~2× fewer allocations in css transitions (snapshot styles once, not per property) finished css animations no longer re-layout every frame less GC in css sibling/child selector queries (no list copies or wrappers) less GC diffing active css rules (no LINQ or hashsets) faster, lower-GC Yoga layout wrapper css stylesheet hotload now watches newly imported files
379 lines
7.8 KiB
C#
379 lines
7.8 KiB
C#
namespace Sandbox.UI;
|
|
|
|
/// <summary>
|
|
/// A CSS selector like "Panel.button.red:hover .text"
|
|
/// </summary>
|
|
[SkipHotload]
|
|
public sealed class StyleSelector
|
|
{
|
|
public StyleBlock Block;
|
|
|
|
public string AsString;
|
|
public string[] Classes { get; private set; }
|
|
|
|
public string Element;
|
|
|
|
/// <summary>
|
|
/// The Id selector - minus the #
|
|
/// https://developer.mozilla.org/en-US/docs/Web/CSS/ID_selectors
|
|
/// </summary>
|
|
public string Id { get; set; }
|
|
public PseudoClass Flags;
|
|
|
|
/// <summary>
|
|
/// Descendant combinator
|
|
/// A B
|
|
/// Child combinator
|
|
/// A > B
|
|
/// Adjacent sibling combinator
|
|
/// A + B
|
|
/// General sibling combinator
|
|
/// A ~B
|
|
/// </summary>
|
|
public StyleSelector Parent;
|
|
public StyleSelector Not;
|
|
public bool ImmediateParent;
|
|
|
|
/// <summary>
|
|
/// True if this has a universal selector (*)
|
|
/// </summary>
|
|
public bool UniversalSelector;
|
|
|
|
/// <summary>
|
|
/// For + combinator
|
|
/// </summary>
|
|
public bool AdjacentSibling;
|
|
|
|
/// <summary>
|
|
/// For ~ combinator
|
|
/// </summary>
|
|
public bool GeneralSibling;
|
|
|
|
public StyleSelector[] AnyOf;
|
|
public StyleSelector[] DecendantOf;
|
|
public StyleSelector[] Has;
|
|
public int SelfScore;
|
|
|
|
public Func<IStyleTarget, bool> NthChild;
|
|
|
|
public int Score
|
|
{
|
|
get
|
|
{
|
|
if ( Block != null )
|
|
{
|
|
// Scale specificity well above any LoadOrder so source order can never outrank it
|
|
return Block.LoadOrder + SelfScore * 100000;
|
|
}
|
|
|
|
return SelfScore;
|
|
}
|
|
}
|
|
|
|
// publuc ParentType [ Ascendant, Parent, Adjacent Sibling, General Sibling ]
|
|
|
|
internal void SetClasses( string[] classes )
|
|
{
|
|
Classes = classes;
|
|
}
|
|
|
|
public void Finalize( StyleBlock block )
|
|
{
|
|
Block = block;
|
|
|
|
UpdateScore();
|
|
}
|
|
|
|
int UpdateScore()
|
|
{
|
|
SelfScore = 0;
|
|
|
|
//
|
|
// https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Cascade_and_inheritance
|
|
//
|
|
|
|
// Id is the holy grail
|
|
// "The selector with the greater value in the ID column wins no matter what the values are in the other columns"
|
|
if ( Id != null ) SelfScore += 1000;
|
|
|
|
// Elements score the lowest
|
|
if ( Element != null ) SelfScore += 1;
|
|
|
|
// Each class is counted as 10
|
|
if ( Classes != null ) SelfScore += Classes.Count() * 10;
|
|
|
|
// If we have any flags, count each one as 10
|
|
if ( Flags != 0 ) SelfScore += ((int)Flags).BitsSet() * 10;
|
|
|
|
// Nth child counts as a flag
|
|
if ( NthChild != null ) SelfScore += 10;
|
|
|
|
// :not doesn't count as anything special, but we do count its content score
|
|
if ( Not != null ) SelfScore += Not.UpdateScore();
|
|
|
|
// This isn't actually perfectly accurate and you might be able to engineer weird situations
|
|
// but it should be fine 99% of the time. If it isn't then the move is to get rid of AnyOf and
|
|
// whatever and collapse the & stuff into individual rules
|
|
if ( AnyOf != null && AnyOf.Length > 0 ) SelfScore += AnyOf.Max( x => x.UpdateScore() );
|
|
if ( DecendantOf != null && DecendantOf.Length > 0 ) SelfScore += DecendantOf.Max( x => x.UpdateScore() );
|
|
|
|
// Add our parents score
|
|
if ( Parent != null ) SelfScore += Parent.UpdateScore();
|
|
|
|
//
|
|
// You'd think things like .a > .b would score higher than .a .b, but that isn't true
|
|
//
|
|
|
|
return SelfScore;
|
|
}
|
|
|
|
public bool TestBroadphase( IStyleTarget target )
|
|
{
|
|
//
|
|
// If we have element names
|
|
//
|
|
if ( Element != null )
|
|
{
|
|
if ( target.ElementName != Element )
|
|
return false;
|
|
}
|
|
|
|
//
|
|
// Id check
|
|
//
|
|
if ( Id != null )
|
|
{
|
|
if ( target.Id == null )
|
|
return false;
|
|
|
|
if ( string.Compare( target.Id, Id, true ) != 0 )
|
|
return false;
|
|
}
|
|
|
|
//
|
|
// If we have a class, you better have one too
|
|
//
|
|
if ( Classes != null )
|
|
{
|
|
if ( !target.HasClasses( Classes ) )
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test whether target passes our selector test. We use forceFlag to do alternate tests for flags like ::before and ::after.
|
|
/// It's basically added to the target's pseudo class list for the test.
|
|
/// </summary>
|
|
public bool Test( IStyleTarget target, PseudoClass forceFlag = PseudoClass.None )
|
|
{
|
|
var pseudo = target.PseudoClass | forceFlag;
|
|
|
|
// Not optional - need to match!
|
|
if ( pseudo.Contains( PseudoClass.Before ) && !Flags.Contains( PseudoClass.Before ) ) return false;
|
|
if ( pseudo.Contains( PseudoClass.After ) && !Flags.Contains( PseudoClass.After ) ) return false;
|
|
|
|
//
|
|
// If we have flags, you better match them
|
|
//
|
|
if ( Flags != PseudoClass.None && (pseudo & Flags) != Flags )
|
|
return false;
|
|
|
|
//
|
|
// :nth-child( 2 )
|
|
//
|
|
if ( NthChild != null && !NthChild( target ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( !TestBroadphase( target ) )
|
|
return false;
|
|
|
|
if ( Parent != null )
|
|
{
|
|
if ( AdjacentSibling )
|
|
{
|
|
// A + B: B's immediately-preceding sibling must match A
|
|
var prev = GetPreviousSibling( target );
|
|
if ( prev == null || !Parent.Test( prev ) )
|
|
return false;
|
|
}
|
|
else if ( GeneralSibling )
|
|
{
|
|
// A ~ B: some preceding sibling of B must match A
|
|
var prev = GetPreviousSibling( target );
|
|
bool matched = false;
|
|
|
|
while ( prev != null )
|
|
{
|
|
if ( Parent.Test( prev ) )
|
|
{
|
|
matched = true;
|
|
break;
|
|
}
|
|
|
|
prev = GetPreviousSibling( prev );
|
|
}
|
|
|
|
if ( !matched )
|
|
return false;
|
|
}
|
|
else if ( !Parent.TestParent( target.Parent, !ImmediateParent ) )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ( Has != null && Has.Length > 0 )
|
|
{
|
|
if ( !TestHas( target ) )
|
|
return false;
|
|
}
|
|
|
|
if ( DecendantOf != null )
|
|
{
|
|
bool passed = false;
|
|
foreach ( var p in DecendantOf )
|
|
{
|
|
if ( !p.TestParent( target.Parent, !ImmediateParent ) )
|
|
continue;
|
|
|
|
passed = true;
|
|
break;
|
|
}
|
|
|
|
if ( !passed ) return false;
|
|
}
|
|
|
|
if ( AnyOf != null )
|
|
{
|
|
bool passed = false;
|
|
foreach ( var p in AnyOf )
|
|
{
|
|
if ( !p.Test( target ) )
|
|
continue;
|
|
|
|
passed = true;
|
|
break;
|
|
}
|
|
|
|
if ( !passed ) return false;
|
|
}
|
|
|
|
if ( Not != null )
|
|
{
|
|
if ( Not.Test( target ) )
|
|
return false;
|
|
}
|
|
|
|
// UniversalSelector
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
public bool TestParent( IStyleTarget target, bool recusive = true )
|
|
{
|
|
if ( target == null )
|
|
return false;
|
|
|
|
if ( Test( target ) )
|
|
return true;
|
|
|
|
if ( !recusive )
|
|
return false;
|
|
|
|
return TestParent( target.Parent );
|
|
}
|
|
|
|
private bool TestHas( IStyleTarget target )
|
|
{
|
|
foreach ( var hasSelector in Has )
|
|
{
|
|
if ( hasSelector.AdjacentSibling )
|
|
{
|
|
var nextSibling = GetNextSibling( target );
|
|
|
|
if ( nextSibling != null && hasSelector.Test( nextSibling ) )
|
|
return true;
|
|
}
|
|
else if ( hasSelector.GeneralSibling )
|
|
{
|
|
var sibling = GetNextSibling( target );
|
|
|
|
while ( sibling != null )
|
|
{
|
|
if ( hasSelector.Test( sibling ) )
|
|
return true;
|
|
|
|
sibling = GetNextSibling( sibling );
|
|
}
|
|
}
|
|
else if ( hasSelector.ImmediateParent )
|
|
{
|
|
foreach ( var child in target.Children ?? Enumerable.Empty<IStyleTarget>() )
|
|
{
|
|
if ( hasSelector.Test( child ) )
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( TestDescendants( target, hasSelector ) )
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TestDescendants( IStyleTarget target, StyleSelector selector )
|
|
{
|
|
var children = target.Children;
|
|
if ( children == null ) return false;
|
|
|
|
foreach ( var child in children )
|
|
{
|
|
if ( selector.Test( child ) )
|
|
return true;
|
|
|
|
if ( TestDescendants( child, selector ) )
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private IStyleTarget GetNextSibling( IStyleTarget target )
|
|
{
|
|
var siblings = target.Parent?.Children;
|
|
if ( siblings == null )
|
|
return null;
|
|
|
|
for ( int i = 0; i < siblings.Count; i++ )
|
|
{
|
|
if ( siblings[i] == target )
|
|
return i + 1 < siblings.Count ? siblings[i + 1] : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private IStyleTarget GetPreviousSibling( IStyleTarget target )
|
|
{
|
|
var siblings = target.Parent?.Children;
|
|
if ( siblings == null )
|
|
return null;
|
|
|
|
for ( int i = 0; i < siblings.Count; i++ )
|
|
{
|
|
if ( siblings[i] == target )
|
|
return i > 0 ? siblings[i - 1] : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|