Files
sbox-public/engine/Sandbox.Engine/Systems/UI/Styles/Styles.Set.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

2264 lines
49 KiB
C#

using System.Collections.Immutable;
using System.Text;
namespace Sandbox.UI
{
public partial class Styles
{
public override bool Set( string property, string value )
{
if ( property == "color" )
property = "font-color";
switch ( property )
{
case "transition":
case "transition-delay":
case "transition-duration":
case "transition-property":
case "transition-timing-function":
Transitions = TransitionDesc.ParseProperty( property, value, Transitions );
return true;
case "display":
return SetDisplay( value );
case "pointer-events":
return SetPointerEvents( value );
case "position":
return SetPosition( value );
case "flex-direction":
return SetFlexDirction( value );
case "justify-content":
return SetJustifyContent( value );
case "flex-wrap":
return SetFlexWrap( value );
case "flex":
return SetFlex( value );
case "gap":
return SetGap( value );
case "padding":
return SetPadding( value );
case "margin":
return SetMargin( value );
case "border-radius":
return SetBorderRadius( value );
case "border":
return SetBorder( value, w => BorderWidth = w, c => BorderColor = c );
case "border-left":
return SetBorder( value, w => BorderLeftWidth = w, c => BorderLeftColor = c );
case "border-right":
return SetBorder( value, w => BorderRightWidth = w, c => BorderRightColor = c );
case "border-top":
return SetBorder( value, w => BorderTopWidth = w, c => BorderTopColor = c );
case "border-bottom":
return SetBorder( value, w => BorderBottomWidth = w, c => BorderBottomColor = c );
case "border-image":
return SetBorderImage( value );
case "border-color":
Color? borderColor = Color.Parse( value );
BorderColor = borderColor;
return borderColor.HasValue;
case "border-width":
Length? borderWidth = Length.Parse( value );
BorderWidth = borderWidth;
return borderWidth.HasValue;
case "backdrop-filter":
return SetBackdropFilter( value );
case "filter":
return SetFilter( value );
case "font-weight":
return SetFontWeight( value );
case "box-shadow":
return SetShadow( value, ref BoxShadow );
case "text-shadow":
return SetShadow( value, ref TextShadow );
case "filter-drop-shadow":
return SetShadow( value, ref FilterDropShadow );
case "align-content":
AlignContent = GetAlign( value );
return AlignContent.HasValue;
case "align-self":
AlignSelf = GetAlign( value );
return AlignSelf.HasValue;
case "align-items":
AlignItems = GetAlign( value );
return AlignItems.HasValue;
case "text-align":
return SetTextAlign( value );
case "text-overflow":
return SetTextOverflow( value );
case "text-filter":
return SetTextFilter( value );
case "word-break":
return SetWordBreak( value );
case "text-decoration":
return SetTextDecoration( value );
case "text-decoration-line":
return SetTextDecorationLine( value );
case "text-decoration-skip-ink":
return SetTextDecorationSkipInk( value );
case "text-decoration-style":
return SetTextDecorationStyle( value );
case "text-stroke":
return SetTextStroke( value );
case "text-transform":
return SetTextTransform( value );
case "font-style":
return SetFontStyle( value );
case "white-space":
return SetWhiteSpace( value );
case "transform":
return SetTransform( value );
case "transform-origin":
return SetTransformOrigin( value );
case "perspective-origin":
return SetPerspectiveOrigin( value );
case "background":
return SetBackground( value );
case "background-image":
return SetImage( value, SetBackgroundImageFromTexture, SetBackgroundSize, SetBackgroundRepeat, SetBackgroundAngle );
case "background-size":
return SetBackgroundSize( value );
case "background-position":
return SetBackgroundPosition( value );
case "background-repeat":
return SetBackgroundRepeat( value );
case "background-image-tint":
property = "background-tint";
break;
case "image-rendering":
return SetImageRendering( value );
case "font-color":
return SetFontColor( value );
case "caret-color":
return SetCaretColor( value );
case "animation-iteration-count":
if ( value == "infinite" )
{
AnimationIterationCount = float.PositiveInfinity;
return true;
}
break;
case "animation":
return SetAnimation( value );
case "mask":
return SetMask( value );
case "mask-image":
return SetImage( value, SetMaskImageFromTexture, SetMaskSize, SetMaskRepeat, SetMaskAngle );
case "mask-mode":
return SetMaskMode( value );
case "mask-size":
return SetMaskSize( value );
case "mask-repeat":
return SetMaskRepeat( value );
case "mask-position":
return SetMaskPosition( value );
case "mask-scope":
return SetMaskScope( value );
case "font-smooth":
return SetFontSmooth( value );
case "object-fit":
return SetObjectFit( value );
}
return base.Set( property, value );
}
enum EBorderImageParseType
{
ParseSlice,
ParseWidth
};
bool SetFontColor( string value )
{
Color? fontColor = Color.Parse( value );
if ( fontColor.HasValue )
{
FontColor = fontColor;
return true;
}
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
if ( GetTokenValueUnderParenthesis( p, "linear-gradient", out string gradient ) )
{
SetTextGradientLinear( gradient );
return true;
}
if ( GetTokenValueUnderParenthesis( p, "radial-gradient", out string radialGradint ) )
{
SetTextGradientRadial( radialGradint );
return true;
}
return false;
}
bool SetCaretColor( string value )
{
Color? caretColor = Color.Parse( value );
if ( caretColor.HasValue )
{
CaretColor = caretColor;
return true;
}
return false;
}
bool SetBorderTexture( Lazy<Texture> t )
{
_borderImageSource = t;
return true;
}
bool SetBorderImage( string value )
{
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
if ( !SetImage( p.Text, SetBorderTexture ) )
throw new Exception( "Expected image as first border-image parameter." );
p.Pointer += p.ReadUntilOrEnd( ")" ).Length + 1;
List<Length> borderSliceList = new List<Length>();
List<Length> borderWidthList = new List<Length>();
EBorderImageParseType parseType = EBorderImageParseType.ParseSlice;
while ( !p.IsEnd )
{
if ( p.Is( "stretch", 0, true ) )
{
p.Pointer += "stretch".Length;
BorderImageRepeat = UI.BorderImageRepeat.Stretch;
}
else if ( p.Is( "round", 0, true ) )
{
p.Pointer += "round".Length;
BorderImageRepeat = UI.BorderImageRepeat.Round;
}
else if ( p.Is( "fill", 0, true ) )
{
p.Pointer += "fill".Length;
BorderImageFill = UI.BorderImageFill.Filled;
}
else if ( p.Is( "/", 0, true ) )
{
p.Pointer++;
//Needs to have at least one element before we do it
if ( borderSliceList.Count == 0 )
throw new Exception( "border-image needs at least one value before splitting ('/')" );
//We don't support anything else
if ( parseType == EBorderImageParseType.ParseWidth )
throw new Exception( "border-image only supports up to slice and width params for splitting('/')" );
parseType++;
}
else if ( p.TryReadLength( out Length lengthValue ) )
{
switch ( parseType )
{
case EBorderImageParseType.ParseSlice:
borderSliceList.Add( lengthValue );
break;
case EBorderImageParseType.ParseWidth:
borderWidthList.Add( lengthValue );
break;
}
}
if ( p.IsEnd )
break;
p.Pointer++;
p.SkipWhitespaceAndNewlines();
}
//Parse our border slice pixel sizes
switch ( borderSliceList.Count )
{
// 33.3% of texture size
case 0:
BorderImageWidthLeft = BorderImageWidthRight = BorderImageWidthTop = BorderImageWidthBottom = BorderImageSource.Width / 3.0f;
break;
//Uniform
case 1:
BorderImageWidthLeft = BorderImageWidthRight = BorderImageWidthTop = BorderImageWidthBottom = borderSliceList[0];
break;
// Top-Bottom and Left-Right
case 2:
BorderImageWidthTop = BorderImageWidthBottom = borderSliceList[0];
BorderImageWidthLeft = BorderImageWidthRight = borderSliceList[1];
break;
// Top, Left-Right and Bottom
case 3:
BorderImageWidthTop = borderSliceList[0];
BorderImageWidthLeft = BorderImageWidthRight = borderSliceList[1];
BorderImageWidthBottom = borderSliceList[2];
break;
// Top, Right, Bottom, Left
case 4:
BorderImageWidthTop = borderSliceList[0];
BorderImageWidthRight = borderSliceList[1];
BorderImageWidthBottom = borderSliceList[2];
BorderImageWidthLeft = borderSliceList[3];
break;
}
//Parse our border width pixel sizes, we re use BorderWidth so we don't need to pass another uniform to the shader
switch ( borderWidthList.Count )
{
//Just copy whwatever is on slice if nothing is set
case 0:
BorderLeftWidth = BorderImageWidthLeft;
BorderRightWidth = BorderImageWidthRight;
BorderTopWidth = BorderImageWidthTop;
BorderBottomWidth = BorderImageWidthBottom;
break;
//Uniform
case 1:
BorderLeftWidth = BorderRightWidth = BorderTopWidth = BorderBottomWidth = borderWidthList[0];
break;
// Top-Bottom and Left-Right
case 2:
BorderTopWidth = BorderBottomWidth = borderWidthList[0];
BorderLeftWidth = BorderRightWidth = borderWidthList[1];
break;
// Top, Left-Right and Bottom
case 3:
BorderTopWidth = borderWidthList[0];
BorderLeftWidth = BorderRightWidth = borderWidthList[1];
BorderBottomWidth = borderWidthList[2];
break;
// Top, Right, Bottom, Left
case 4:
BorderTopWidth = borderWidthList[0];
BorderRightWidth = borderWidthList[1];
BorderBottomWidth = borderWidthList[2];
BorderLeftWidth = borderWidthList[3];
break;
}
return true;
}
bool SetBorderRadius( string value )
{
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd )
return false;
if ( !p.TryReadLength( out var a ) )
return false;
if ( p.IsEnd || !p.TryReadLength( out var b ) )
{
BorderTopLeftRadius = a;
BorderTopRightRadius = a;
BorderBottomRightRadius = a;
BorderBottomLeftRadius = a;
return true;
}
if ( p.IsEnd || !p.TryReadLength( out var c ) )
{
BorderTopLeftRadius = a;
BorderTopRightRadius = b;
BorderBottomRightRadius = a;
BorderBottomLeftRadius = b;
return true;
}
if ( p.IsEnd || !p.TryReadLength( out var d ) )
{
BorderTopLeftRadius = a;
BorderTopRightRadius = b;
BorderBottomRightRadius = c;
BorderBottomLeftRadius = b;
return true;
}
BorderTopLeftRadius = a;
BorderTopRightRadius = b;
BorderBottomRightRadius = c;
BorderBottomLeftRadius = d;
return true;
}
bool SetMask( string value )
{
/*
* mask: <mask-reference> || <position> [ / <bg-size> ]? ||<repeat-style> || <geometry-box> || [ <geometry-box> | no-clip ] || <compositing-operator> || <masking-mode>
* https://developer.mozilla.org/en-US/docs/Web/CSS/mask#formal_syntax
*
* mask: url(mask.png);
* mask: url(mask.png) luminance;
* mask: url(mask.png) 100px 200px;
* mask: url(mask.png) 100px 200px/50px 100px;
* mask: url(mask.png) repeat-x;
* mask: url(mask.png) 50% 50% / contain no-repeat border-box luminance;
*/
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
//
// <mask-reference>
//
if ( !SetImage( p.Text, SetMaskImageFromTexture, SetMaskSize, SetMaskRepeat, SetMaskAngle ) )
throw new Exception( "Expected image as first border-image parameter." );
p.Pointer += p.ReadUntilOrEnd( ")" ).Length + 1;
p = p.SkipWhitespaceAndNewlines();
//
// <position> [ / <bg-size> ]?
//
p.TryReadPositionAndSize( out var positionX, out var positionY, out var sizeX, out var sizeY );
MaskPositionX = positionX;
MaskPositionY = positionY;
if ( sizeX.Unit != LengthUnit.Auto ) MaskSizeX = sizeX;
if ( sizeY.Unit != LengthUnit.Auto ) MaskSizeY = sizeY;
//
// <repeat-style>
//
if ( p.TryReadRepeat( out var repeat ) )
SetMaskRepeat( repeat );
//
// <masking-mode>
//
if ( p.TryReadMaskMode( out var maskMode ) )
SetMaskMode( maskMode );
return true;
}
bool SetFlex( string value )
{
/*
* flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
* https://drafts.csswg.org/css-flexbox/#flex-property
*/
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
int floatCount = 0;
while ( !p.IsEnd )
{
var word = p.ReadWord( " ", true ).ToLower();
p.Pointer -= word.Length;
if ( word == "none" )
{
// "none" expands to 0 0 auto
FlexShrink ??= 0;
FlexGrow ??= 0;
FlexBasis = Length.Auto;
return true;
}
else if ( word == "auto" )
{
// "auto" expands to 1 1 auto
FlexShrink ??= 1;
FlexGrow ??= 1;
FlexBasis = Length.Auto;
return true;
}
else if ( word == "initial" )
{
// "initial" expands to 0 1 auto
FlexShrink ??= 0;
FlexGrow ??= 1;
FlexBasis = Length.Auto;
return true;
}
else
{
var maybeLength = p;
var maybeFloat = p.ReadUntilWhitespaceOrNewlineOrEnd();
// TryReadFloat eats lengths, TryReadLength eats floats
// settle it with this
if ( float.TryParse( maybeFloat, out float val ) )
{
if ( floatCount == 0 )
{
FlexGrow = val;
// "flex: 1" expands to <number [1]> 1 0
if ( val == 1 )
{
FlexShrink = 1;
FlexBasis = 0;
}
}
else
{
FlexShrink = val;
}
floatCount++;
}
else if ( maybeLength.TryReadLength( out var len ) )
{
FlexGrow ??= 0;
FlexShrink ??= 1;
FlexBasis = len;
return true;
}
else
{
Log.Error( $"Couldn't parse flex {value} - expected a float or length" );
return false;
}
}
p.SkipWhitespaceAndNewlines();
}
return true;
}
bool SetFilterBorderWrap( string value )
{
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
while ( !p.IsEnd )
{
if ( p.TryReadLength( out var lengthValue ) )
FilterBorderWidth = lengthValue;
else if ( p.TryReadColor( out var colorValue ) )
FilterBorderColor = colorValue;
else
return false;
p = p.SkipWhitespaceAndNewlines();
}
return true;
}
bool SetBorder( string value, Action<Length?> setWidth, Action<Color?> setColor )
{
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
while ( !p.IsEnd )
{
if ( p.TryReadLineStyle( out var lineStyle ) )
{
if ( lineStyle == "none" )
{
setWidth( Length.Pixels( 0 ) );
return true;
}
}
else if ( p.TryReadLength( out var lengthValue ) )
{
setWidth( lengthValue );
}
else if ( p.TryReadColor( out var colorValue ) )
{
setColor( colorValue );
}
else
{
return false;
}
p = p.SkipWhitespaceAndNewlines();
}
return true;
}
bool SetPadding( string value )
{
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return false;
if ( p.TryReadLength( out var a ) )
{
Padding = a;
}
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return true;
if ( p.TryReadLength( out var b ) )
{
PaddingLeft = b;
PaddingRight = b;
}
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return true;
if ( p.TryReadLength( out var c ) )
{
PaddingBottom = c;
}
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return true;
if ( p.TryReadLength( out var d ) )
{
PaddingTop = a;
PaddingRight = b;
PaddingBottom = c;
PaddingLeft = d;
}
return true;
}
bool SetMargin( string value )
{
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return false;
if ( p.TryReadLength( out var a ) )
{
MarginLeft = a;
MarginTop = a;
MarginRight = a;
MarginBottom = a;
}
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return true;
if ( p.TryReadLength( out var b ) )
{
MarginLeft = b;
MarginRight = b;
}
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return true;
if ( p.TryReadLength( out var c ) )
{
MarginBottom = c;
}
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return true;
if ( p.TryReadLength( out var d ) )
{
MarginTop = a;
MarginRight = b;
MarginBottom = c;
MarginLeft = d;
}
return true;
}
bool SetFontWeight( string value )
{
if ( int.TryParse( value, out var i ) )
{
FontWeight = i;
return true;
}
switch ( value )
{
case "hairline":
case "thin":
FontWeight = 100;
return true;
case "ultralight":
case "extralight":
FontWeight = 200;
return true;
case "light":
FontWeight = 300;
return true;
case "regular":
case "normal":
FontWeight = 400;
return true;
case "medium":
FontWeight = 500;
return true;
case "demibold":
case "semibold":
FontWeight = 600;
return true;
case "bold":
FontWeight = 700;
return true;
case "ultabold":
case "extrabold":
FontWeight = 800;
return true;
case "heavy":
case "black":
FontWeight = 900;
return true;
case "extrablack":
case "ultrablack":
FontWeight = 950;
return true;
// TODO: These should change depending on the parents value.
case "bolder":
// Parent 100->300 = 400 weight
// Parent 400->500 = 700 weight
// Parent 600+ = 900 Weight
FontWeight = 900;
return true;
case "lighter":
// Parent 100->500 = 100 weight
// Parent 600->700 = 400 weight
// Parent 800+ = 700 weight
FontWeight = 200;
return true;
}
return false;
}
bool SetShadow( string value, ref ShadowList shadowList )
{
var p = new Parse( value );
shadowList.Clear();
if ( p.Is( "none", 0, true ) )
{
shadowList.IsNone = true;
return true;
}
while ( !p.IsEnd )
{
var shadow = new Shadow();
if ( !p.TryReadLength( out var x ) )
return false;
if ( !p.TryReadLength( out var y ) )
return false;
shadow.OffsetX = x.Value;
shadow.OffsetY = y.Value;
if ( p.TryReadLength( out var blur ) )
{
shadow.Blur = blur.Value;
if ( p.TryReadLength( out var spread ) )
{
shadow.Spread = spread.Value;
}
}
if ( p.TryReadColor( out var color ) )
{
shadow.Color = color;
}
p.SkipWhitespaceAndNewlines();
if ( p.TryReadShadowInset( out var inset ) )
{
shadow.Inset = inset;
}
shadowList.Add( shadow );
p.SkipWhitespaceAndNewlines();
if ( p.IsEnd || p.Current != ',' )
return true;
p.Pointer++;
p.SkipWhitespaceAndNewlines();
}
return true;
}
bool SetTextStroke( string value )
{
var p = new Parse( value );
if ( !p.TryReadLength( out var width ) )
return false;
if ( !p.TryReadColor( out var color ) )
return false;
TextStrokeWidth = width;
TextStrokeColor = color;
return true;
}
bool SetDisplay( string value )
{
switch ( value )
{
case "none":
Display = DisplayMode.None;
return true;
case "flex":
Display = DisplayMode.Flex;
return true;
case "contents":
Display = DisplayMode.Contents;
return true;
default:
Log.Warning( $"Unhandled display property: {value}" );
return false;
}
}
bool SetPointerEvents( string value )
{
switch ( value )
{
case "auto":
PointerEvents = null;
return true;
case "none":
PointerEvents = UI.PointerEvents.None;
return true;
case "all":
PointerEvents = UI.PointerEvents.All;
return true;
default:
Log.Warning( $"Unhandled pointer-events value: {value} (expected auto, none, all)" );
return false;
}
}
bool SetPosition( string value )
{
switch ( value )
{
case "static":
Position = PositionMode.Static;
return true;
case "absolute":
Position = PositionMode.Absolute;
return true;
case "relative":
Position = PositionMode.Relative;
return true;
default:
Log.Warning( $"Unhandled position property: {value}" );
return false;
}
}
bool SetFlexDirction( string value )
{
switch ( value )
{
case "column":
FlexDirection = UI.FlexDirection.Column;
return true;
case "column-reverse":
FlexDirection = UI.FlexDirection.ColumnReverse;
return true;
case "row":
FlexDirection = UI.FlexDirection.Row;
return true;
case "row-reverse":
FlexDirection = UI.FlexDirection.RowReverse;
return true;
default:
Log.Warning( $"Unhandled flex-direction property: {value}" );
return false;
}
}
bool SetFlexWrap( string value )
{
switch ( value )
{
case "nowrap":
FlexWrap = Wrap.NoWrap;
return true;
case "wrap":
FlexWrap = Wrap.Wrap;
return true;
case "wrap-reverse":
FlexWrap = Wrap.WrapReverse;
return true;
default:
Log.Warning( $"Unhandled flex-wrap property: {value}" );
return false;
}
}
bool SetGap( string value )
{
// gap =
// < 'row-gap' > < 'column-gap' >?
var p = new Parse( value );
if ( !p.TryReadLength( out var gap ) )
return false;
RowGap = gap;
ColumnGap = gap;
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return true;
if ( !p.TryReadLength( out var colGap ) )
return false;
ColumnGap = colGap;
return true;
}
bool SetJustifyContent( string value )
{
switch ( value )
{
case "flex-start":
JustifyContent = UI.Justify.FlexStart;
return true;
case "center":
JustifyContent = UI.Justify.Center;
return true;
case "flex-end":
JustifyContent = UI.Justify.FlexEnd;
return true;
case "space-between":
JustifyContent = UI.Justify.SpaceBetween;
return true;
case "space-around":
JustifyContent = UI.Justify.SpaceAround;
return true;
case "space-evenly":
JustifyContent = UI.Justify.SpaceEvenly;
return true;
default:
Log.Warning( $"Unhandled justify-content property: {value}" );
return false;
}
}
private Align? GetAlign( string value )
{
switch ( value )
{
case "auto": return Align.Auto;
case "flex-end": return Align.FlexEnd;
case "flex-start": return Align.FlexStart;
case "center": return Align.Center;
case "stretch": return Align.Stretch;
case "space-between": return Align.SpaceBetween;
case "space-around": return Align.SpaceAround;
case "space-evenly": return Align.SpaceEvenly;
case "baseline": return Align.Baseline;
default:
Log.Warning( $"Unhandled align property: {value}" );
return null;
}
}
bool SetTextAlign( string value )
{
switch ( value )
{
case "center":
TextAlign = UI.TextAlign.Center;
return true;
case "left":
TextAlign = UI.TextAlign.Left;
return true;
case "right":
TextAlign = UI.TextAlign.Right;
return true;
default:
Log.Warning( $"Unhandled text-align property: {value}" );
return false;
}
}
bool SetTextOverflow( string value )
{
switch ( value )
{
case "ellipsis":
TextOverflow = UI.TextOverflow.Ellipsis;
return true;
case "clip":
TextOverflow = UI.TextOverflow.Clip;
return true;
default:
Log.Warning( $"Unhandled text-overflow property: {value}" );
return false;
}
}
bool SetTextFilter( string value )
{
switch ( value )
{
case "linear":
case "bilinear":
TextFilter = Rendering.FilterMode.Bilinear;
return true;
case "point":
TextFilter = Rendering.FilterMode.Point;
return true;
case "trilinear":
TextFilter = Rendering.FilterMode.Trilinear;
return true;
case "anisotropic":
TextFilter = Rendering.FilterMode.Anisotropic;
return true;
default:
Log.Warning( $"Unhandled text-filter property: {value}" );
return false;
}
}
bool SetWordBreak( string value )
{
switch ( value )
{
case "normal":
WordBreak = UI.WordBreak.Normal;
return true;
case "break-all":
WordBreak = UI.WordBreak.BreakAll;
return true;
default:
Log.Warning( $"Unhandled word-break property: {value}" );
return false;
}
}
UI.TextDecoration GetTextDecorationFromValue( string value )
{
var td = UI.TextDecoration.None;
if ( value.Contains( "underline" ) ) td |= UI.TextDecoration.Underline;
if ( value.Contains( "line-through" ) ) td |= UI.TextDecoration.LineThrough;
if ( value.Contains( "overline" ) ) td |= UI.TextDecoration.Overline;
return td;
}
bool SetTextDecoration( string value )
{
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return false;
var td = UI.TextDecoration.None;
while ( !p.IsEnd )
{
p = p.SkipWhitespaceAndNewlines();
if ( p.TryReadLength( out var decorationThickness ) )
{
TextDecorationThickness = decorationThickness;
continue;
}
if ( p.TryReadColor( out var decorationColor ) )
{
TextDecorationColor = decorationColor;
continue;
}
var subValue = p.ReadWord( null, true );
var textDecoration = GetTextDecorationFromValue( subValue );
if ( textDecoration != UI.TextDecoration.None )
{
td |= textDecoration;
continue;
}
if ( !SetTextDecorationStyle( subValue ) )
{
return false;
}
}
if ( td != UI.TextDecoration.None )
{
TextDecorationLine = td;
}
return true;
}
bool SetTextDecorationLine( string value )
{
TextDecorationLine = GetTextDecorationFromValue( value );
return true;
}
bool SetTextDecorationSkipInk( string value )
{
switch ( value )
{
case "auto":
case "all":
TextDecorationSkipInk = UI.TextSkipInk.All;
return true;
case "none":
TextDecorationSkipInk = UI.TextSkipInk.None;
return true;
default:
Log.Warning( $"Unhandled text-decoration-skip-ink property: {value}" );
return false;
}
}
bool SetTextDecorationStyle( string value )
{
switch ( value )
{
case "solid":
TextDecorationStyle = UI.TextDecorationStyle.Solid;
return true;
case "double":
TextDecorationStyle = UI.TextDecorationStyle.Double;
return true;
case "dotted":
TextDecorationStyle = UI.TextDecorationStyle.Dotted;
return true;
case "dashed":
TextDecorationStyle = UI.TextDecorationStyle.Dashed;
return true;
case "wavy":
TextDecorationStyle = UI.TextDecorationStyle.Wavy;
return true;
default:
Log.Warning( $"Unhandled text-decoration-style property: {value}" );
return false;
}
}
bool SetFontStyle( string value )
{
var fs = UI.FontStyle.None;
if ( value.Contains( "italic" ) ) fs |= UI.FontStyle.Italic;
if ( value.Contains( "oblique" ) ) fs |= UI.FontStyle.Oblique;
FontStyle = fs;
return true;
}
bool SetWhiteSpace( string value )
{
switch ( value )
{
case "normal":
WhiteSpace = UI.WhiteSpace.Normal;
break;
case "nowrap":
WhiteSpace = UI.WhiteSpace.NoWrap;
break;
case "pre-line":
WhiteSpace = UI.WhiteSpace.PreLine;
break;
case "pre":
WhiteSpace = UI.WhiteSpace.Pre;
break;
default:
Log.Warning( $"Unhandled white-space property: {value}" );
return false;
}
return true;
}
bool SetTextTransform( string value )
{
switch ( value )
{
case "capitalize":
TextTransform = UI.TextTransform.Capitalize;
return true;
case "uppercase":
TextTransform = UI.TextTransform.Uppercase;
return true;
case "lowercase":
TextTransform = UI.TextTransform.Lowercase;
return true;
case "none":
TextTransform = UI.TextTransform.None;
return true;
default:
Log.Warning( $"Unhandled text-transform property: {value}" );
return false;
}
}
bool SetTransformOrigin( string value )
{
var p = new Parse( value );
if ( !p.TryReadLength( out var x ) )
return false;
TransformOriginX = x;
if ( !p.TryReadLength( out var y ) )
{
TransformOriginY = x;
return true;
}
TransformOriginY = y;
return true;
}
bool SetTransform( string value )
{
if ( string.IsNullOrEmpty( value ) || value == "none" )
{
Transform = null;
return true;
}
var t = new PanelTransform();
t.Parse( value );
Transform = t;
return true;
}
bool SetPerspectiveOrigin( string value )
{
var p = new Parse( value );
if ( !p.TryReadLength( out var x ) )
return false;
PerspectiveOriginX = x;
if ( !p.TryReadLength( out var y ) )
{
PerspectiveOriginY = x;
return true;
}
// Do we want to move away from standard and let us specify a Z too?
PerspectiveOriginY = y;
return true;
}
bool GetTokenValueUnderParenthesis( Parse p, string tokenName, out string result )
{
if ( p.Is( tokenName, 0, true ) )
{
p.Pointer += tokenName.Length;
p = p.SkipWhitespaceAndNewlines();
if ( p.Current != '(' ) throw new System.Exception( "Expected ( after " + tokenName );
p.Pointer++;
int stack = 1;
var wordStart = p;
while ( !p.IsEnd && stack > 0 )
{
p.Pointer++;
if ( p.Current == '(' ) stack++;
if ( p.Current == ')' ) stack--;
}
if ( p.IsEnd ) throw new System.Exception( "Expected ) after " + tokenName );
result = wordStart.Read( p.Pointer - wordStart.Pointer );
return true;
}
result = "";
return false;
}
bool SetBackground( string value )
{
/*
* We support a version of the "background" syntax that consists only of
* the final background layer; we also omit:
* - background-attachment
* - background-clip
* - background-origin
*
* so our syntax can be defined as:
* background: <bg-image> || <bg-position> [ / <bg-size> ]? || <repeat-style> || <'background-color'>
* https://drafts.csswg.org/css-backgrounds/#the-background
*/
var p = new Parse( value );
var bgBuilder = new StringBuilder();
var lengthList = new List<Length>();
var keywords = new List<string>();
// Values (like linear-gradient(...), #ff00ff, etc.) need special handling - we read those
// until we reach an end bracket, rather than a space
int depth = 0;
while ( !p.IsEnd )
{
p.SkipWhitespaceAndNewlines();
var part = p.ReadWord( " ", true );
if ( part.Contains( "#" ) )
depth++;
depth += part.Count( x => x == '(' );
if ( depth > 0 )
{
depth -= part.Count( x => x == ')' );
bgBuilder.Append( part + " " );
}
else
{
// Ignore separators
if ( part == "/" ) continue;
var length = Length.Parse( part );
if ( length != null ) lengthList.Add( length!.Value );
else if ( part == "repeat-x" || part == "repeat-y" || part == "repeat" || part == "space" || part == "round" || part == "no-repeat" )
{
//
// <repeat-style>
//
SetBackgroundRepeat( part );
}
else
{
Log.Warning( $"Unrecognised part {part} in background" );
}
}
p.SkipWhitespaceAndNewlines();
}
//
// <bg-image> / <background-color>
//
string bgSource = bgBuilder.ToString().Trim();
if ( bgSource.StartsWith( "#" ) || bgSource.StartsWith( "rgb(" ) || bgSource.StartsWith( "hsv(" ) )
BackgroundColor = Color.Parse( bgSource ) ?? default;
else
SetImage( bgSource, SetBackgroundImageFromTexture, SetBackgroundSize, SetBackgroundRepeat, SetBackgroundAngle );
//
// <bg-position> [ / <bg-size> ]?
//
if ( lengthList.Count > 0 )
{
// Position X and Y
BackgroundPositionX = lengthList[0];
BackgroundPositionY = lengthList[0];
switch ( lengthList.Count )
{
case 2:
case 3:
// Size
BackgroundSizeX = lengthList[1];
BackgroundSizeY = lengthList[1];
if ( lengthList.Count == 3 )
{
// Position Y
BackgroundPositionY = lengthList[1];
// Size
BackgroundSizeX = lengthList[2];
BackgroundSizeY = lengthList[2];
}
break;
case 4:
// Position Y, Size X, Size Y
BackgroundPositionY = lengthList[1];
BackgroundSizeX = lengthList[2];
BackgroundSizeY = lengthList[3];
break;
}
}
return true;
}
bool SetAnimation( string value )
{
/* <single-animation> =
* <time [0s,∞]> || <easing-function> || <time> || <single-animation-iteration-count> || <single-animation-direction>
* || <single-animation-fill-mode> || <single-animation-play-state> || [ none | <keyframes-name> ]
*/
var p = new Parse( value );
//
// animation: none;
//
if ( p.Is( "none", 0, true ) )
{
AnimationName = "none";
return true;
}
int timeCount = 0;
while ( !p.IsEnd )
{
p = p.SkipWhitespaceAndNewlines();
// The first value in each <single-animation> that can be parsed as a <time> is assigned to the animation-duration
// The second value in each <single-animation> that can be parsed as a <time> is assigned to animation-delay
if ( p.TryReadTime( out var time ) )
{
if ( timeCount == 0 )
AnimationDuration = time / 1000.0f; // ms to s
else
AnimationDelay = time / 1000.0f; // ms to s
timeCount++;
continue;
}
// When parsing, keywords that are valid for properties other than animation-name whose values were not found earlier
// in the shorthand must be accepted for those properties rather than for animation-name.
var word = p.ReadWord( null, true ).ToLower();
if ( Utility.Easing.TryGetFunction( word, out _ ) )
{
AnimationTimingFunction = word;
}
else if ( int.TryParse( word, out int iterationCount ) || word == "infinite" )
{
if ( word == "infinite" )
AnimationIterationCount = float.PositiveInfinity;
else
AnimationIterationCount = iterationCount;
}
else if ( word == "normal" || word == "reverse" || word == "alternate" || word == "alternate-reverse" )
{
AnimationDirection = word;
}
else if ( word == "none" || word == "forwards" || word == "backwards" || word == "both" )
{
AnimationFillMode = word;
}
else if ( word == "running" || word == "paused" )
{
AnimationPlayState = word;
}
else
{
AnimationName = word;
}
}
return true;
}
/// <param name="value"></param>
/// <param name="setImage">Optional</param>
/// <param name="setSize">Optional</param>
/// <param name="setRepeat">Optional</param>
/// <param name="setAngle">Optional</param>
bool SetImage( string value, Func<Lazy<Texture>, bool> setImage = null, Func<string, bool> setSize = null, Func<string, bool> setRepeat = null, Func<float, bool> setAngle = null )
{
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
// TODO - support for multiple?
if ( p.Is( "none", 0, true ) )
{
setImage( new Lazy<Texture>( Texture.Invalid ) );
return true;
}
if ( GetTokenValueUnderParenthesis( p, "url", out string url ) )
{
url = url.Trim( ' ', '"', '\'' );
setImage( new Lazy<Texture>( () =>
{
return Texture.Load( url ) ?? Texture.Invalid;
} ) );
return true;
}
if ( GetTokenValueUnderParenthesis( p, "linear-gradient", out string gradient ) )
{
Texture gradientTexture = GenerateLinearGradientTexture( gradient, out var angle );
setAngle?.Invoke( angle );
setImage?.Invoke( new Lazy<Texture>( gradientTexture ) );
setSize?.Invoke( "100%" );
setRepeat?.Invoke( "clamp" );
return true;
}
if ( GetTokenValueUnderParenthesis( p, "radial-gradient", out string radialGradient ) )
{
setImage?.Invoke( new Lazy<Texture>( GenerateRadialGradientTexture( radialGradient ) ) );
setSize?.Invoke( "100%" );
setRepeat?.Invoke( "clamp" );
return true;
}
if ( GetTokenValueUnderParenthesis( p, "conic-gradient", out string conicGradient ) )
{
setImage?.Invoke( new Lazy<Texture>( GenerateConicGradientTexture( conicGradient ) ) );
setSize?.Invoke( "100%" );
setRepeat?.Invoke( "clamp" );
return true;
}
if ( GetTokenValueUnderParenthesis( p, "material", out string materialUrl ) )
{
return true;
}
Log.Warning( $"Unknown Image Type \"{value}\"\n" );
return false;
}
bool SetImageRendering( string value )
{
switch ( value )
{
case "auto":
case "anisotropic":
ImageRendering = UI.ImageRendering.Anisotropic;
return true;
case "bilinear":
ImageRendering = UI.ImageRendering.Bilinear;
return true;
case "trilinear":
ImageRendering = UI.ImageRendering.Trilinear;
return true;
case "point":
case "pixelated":
case "nearest-neighbor":
ImageRendering = UI.ImageRendering.Point;
return true;
}
Log.Warning( $"Unknown Image Rendering \"{value}\"\n" );
return false;
}
bool SetBackdropFilter( string value )
{
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
while ( !p.IsEnd )
{
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return true;
var name = p.ReadWord( "(" );
var innervalue = p.ReadInnerBrackets();
if ( name == "blur" )
{
BackdropFilterBlur = Length.Parse( innervalue );
continue;
}
if ( name == "invert" )
{
BackdropFilterInvert = Length.Parse( innervalue );
continue;
}
if ( name == "contrast" )
{
BackdropFilterContrast = Length.Parse( innervalue );
continue;
}
if ( name == "brightness" )
{
BackdropFilterBrightness = Length.Parse( innervalue );
continue;
}
if ( name == "grayscale" )
{
BackdropFilterSaturate = Length.Parse( innervalue );
if ( BackdropFilterSaturate.HasValue )
{
var val = BackdropFilterSaturate.Value.GetPixels( 1 );
BackdropFilterSaturate = 1 - val;
}
continue;
}
if ( name == "saturate" )
{
BackdropFilterSaturate = Length.Parse( innervalue );
continue;
}
if ( name == "sepia" )
{
BackdropFilterSepia = Length.Parse( innervalue );
continue;
}
if ( name == "hue-rotate" )
{
BackdropFilterHueRotate = Length.Parse( innervalue );
continue;
}
return false;
}
return true;
}
bool SetFilter( string value )
{
var p = new Parse( value );
p = p.SkipWhitespaceAndNewlines();
while ( !p.IsEnd )
{
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return true;
var name = p.ReadWord( "(" );
var innervalue = p.ReadInnerBrackets();
switch ( name )
{
case "blur":
FilterBlur = Length.Parse( innervalue );
break;
case "saturate":
FilterSaturate = Length.Parse( innervalue );
break;
case "greyscale":
case "grayscale":
FilterSaturate = Length.Parse( innervalue );
if ( FilterSaturate.HasValue )
{
var val = FilterSaturate.Value.GetPixels( 1 );
FilterSaturate = 1 - val;
}
break;
case "sepia":
FilterSepia = Length.Parse( innervalue );
break;
case "brightness":
FilterBrightness = Length.Parse( innervalue );
break;
case "contrast":
FilterContrast = Length.Parse( innervalue );
break;
case "hue-rotate":
FilterHueRotate = Length.Parse( innervalue );
break;
case "invert":
FilterInvert = Length.Parse( innervalue );
break;
case "tint":
FilterTint = Color.Parse( innervalue );
break;
case "drop-shadow":
var shadowList = new ShadowList();
SetShadow( innervalue, ref shadowList );
FilterDropShadow = shadowList;
break;
case "border-wrap":
SetFilterBorderWrap( innervalue );
break;
default:
Log.Warning( $"Unknown filter property {innervalue}" );
return false;
}
}
return true;
}
bool SetMaskImageFromTexture( Lazy<Texture> texture )
{
if ( texture == null )
return true;
_maskImage = texture;
return true;
}
bool SetMaskMode( string value )
{
switch ( value )
{
case "match-source":
MaskMode = UI.MaskMode.MatchSource;
return true;
case "alpha":
MaskMode = UI.MaskMode.Alpha;
return true;
case "luminance":
MaskMode = UI.MaskMode.Luminance;
return true;
default:
Log.Warning( $"Unhandled mask-mode property: {value}" );
return false;
}
}
bool SetBackgroundImageFromTexture( Lazy<Texture> texture )
{
if ( texture == null )
return true;
_backgroundImage = texture;
Dirty();
return true;
}
bool SetBackgroundAngle( float value )
{
if ( value < 0 )
return false;
BackgroundAngle = value;
return true;
}
bool SetMaskAngle( float value )
{
if ( value < 0 )
return false;
MaskAngle = value;
return true;
}
bool TryParseAngle( string value, out float outAngle )
{
outAngle = 0.0f;
var angle = GetAngleInDegrees( value );
if ( !angle.HasValue ) return false;
var angleDeg = angle.Value;
// The shader expects radians.
var angleRad = angleDeg.Value.DegreeToRadian();
outAngle = angleRad;
return true;
}
bool SetBackgroundSize( string value )
{
var p = new Parse( value );
if ( p.TryReadLength( out var lenx ) )
{
BackgroundSizeX = lenx;
BackgroundSizeY = lenx;
if ( p.TryReadLength( out var leny ) )
{
BackgroundSizeY = leny;
}
}
return true;
}
bool SetMaskPosition( string value )
{
var p = new Parse( value );
if ( p.TryReadLength( out var lenx ) )
{
MaskPositionX = lenx;
MaskPositionY = lenx;
if ( p.TryReadLength( out var leny ) )
{
MaskPositionY = leny;
}
}
return true;
}
bool SetMaskSize( string value )
{
var p = new Parse( value );
if ( p.TryReadLength( out var lenx ) )
{
MaskSizeX = lenx;
MaskSizeY = lenx;
if ( p.TryReadLength( out var leny ) )
{
MaskSizeY = leny;
}
}
return true;
}
bool SetBackgroundPosition( string value )
{
var p = new Parse( value );
if ( p.TryReadLength( out var lenx ) )
{
BackgroundPositionX = lenx;
BackgroundPositionY = lenx;
if ( p.TryReadLength( out var leny ) )
{
BackgroundPositionY = leny;
}
}
return true;
}
bool SetMaskScope( string value )
{
switch ( value )
{
case "default":
MaskScope = Sandbox.UI.MaskScope.Default;
return true;
case "filter":
MaskScope = Sandbox.UI.MaskScope.Filter;
return true;
}
return false;
}
bool SetMaskRepeat( string value )
{
switch ( value )
{
case "no-repeat":
MaskRepeat = Sandbox.UI.BackgroundRepeat.NoRepeat;
return true;
case "repeat-x":
MaskRepeat = Sandbox.UI.BackgroundRepeat.RepeatX;
return true;
case "repeat-y":
MaskRepeat = Sandbox.UI.BackgroundRepeat.RepeatY;
return true;
case "repeat":
MaskRepeat = Sandbox.UI.BackgroundRepeat.Repeat;
return true;
case "round":
case "clamp":
MaskRepeat = Sandbox.UI.BackgroundRepeat.Clamp;
return true;
}
return false;
}
bool SetBackgroundRepeat( string value )
{
switch ( value )
{
case "no-repeat":
BackgroundRepeat = Sandbox.UI.BackgroundRepeat.NoRepeat;
return true;
case "repeat-x":
BackgroundRepeat = Sandbox.UI.BackgroundRepeat.RepeatX;
return true;
case "repeat-y":
BackgroundRepeat = Sandbox.UI.BackgroundRepeat.RepeatY;
return true;
case "repeat":
BackgroundRepeat = Sandbox.UI.BackgroundRepeat.Repeat;
return true;
case "round":
case "clamp":
BackgroundRepeat = Sandbox.UI.BackgroundRepeat.Clamp;
return true;
}
return false;
}
bool SetTextGradientLinear( string gradient )
{
TextGradient = new();
TextGradient.GradientType = GradientInfo.GradientTypes.Linear;
var p = new Parse( gradient );
p.SkipWhitespaceAndNewlines();
var restoreP = p;
if ( !p.TryReadColor( out var _ ) )
{
p = restoreP;
var angle = p.ReadUntilOrEnd( ",", true );
p.SkipWhitespaceAndNewlines( "," );
if ( !string.IsNullOrEmpty( angle ) )
{
SetTextGradientAngle( angle );
}
}
else
{
p = restoreP;
}
var colors = p.ReadRemaining();
var gradientData = ParseGradient( colors );
TextGradient.ColorOffsets = ImmutableArray.Create<GradientColorOffset>();
foreach ( var gen in gradientData )
{
TextGradient.ColorOffsets = TextGradient.ColorOffsets.Add( gen.from );
TextGradient.ColorOffsets = TextGradient.ColorOffsets.Add( gen.to );
}
return true;
}
bool SetTextGradientRadial( string gradient )
{
TextGradient = new();
TextGradient.OffsetX = Length.Percent( 50 ).Value;
TextGradient.OffsetY = Length.Percent( 50 ).Value;
TextGradient.GradientType = GradientInfo.GradientTypes.Radial;
TextGradient.SizeMode = GradientInfo.RadialSizeMode.FarthestSide;
var p = new Parse( gradient );
p.SkipWhitespaceAndNewlines();
var restoreP = p;
if ( !p.TryReadColor( out var _ ) )
{
p = restoreP;
var sizemode = p.ReadUntilOrEnd( ", ", true );
p.SkipWhitespaceAndNewlines();
var position = p.ReadUntilOrEnd( ",", true );
p.SkipWhitespaceAndNewlines( "," );
if ( !string.IsNullOrEmpty( sizemode ) )
{
SetTextGradientSizeMode( sizemode );
}
if ( !string.IsNullOrEmpty( position ) )
{
SetTextGradientPosition( position );
}
}
else
{
p = restoreP;
}
var colors = p.ReadRemaining();
var gradientData = ParseGradient( colors );
TextGradient.ColorOffsets = ImmutableArray.Create<GradientColorOffset>();
foreach ( var gen in gradientData )
{
TextGradient.ColorOffsets = TextGradient.ColorOffsets.Add( gen.from );
TextGradient.ColorOffsets = TextGradient.ColorOffsets.Add( gen.to );
}
return true;
}
bool SetTextGradientPosition( string value )
{
var p = new Parse( value );
if ( p.Is( "at " ) )
p.Pointer += 3;
if ( p.TryReadLength( out var lenx ) )
{
TextGradient.OffsetX = lenx;
TextGradient.OffsetY = lenx;
if ( p.TryReadLength( out var leny ) )
{
TextGradient.OffsetY = leny;
}
return true;
}
return false;
}
bool SetTextGradientSizeMode( string value )
{
switch ( value )
{
case "circle":
TextGradient.SizeMode = GradientInfo.RadialSizeMode.Circle;
return true;
case "closest-corner":
TextGradient.SizeMode = GradientInfo.RadialSizeMode.ClosestCorner;
return true;
case "closest-side":
TextGradient.SizeMode = GradientInfo.RadialSizeMode.ClosestSide;
return true;
case "farthest-corner":
TextGradient.SizeMode = GradientInfo.RadialSizeMode.FarthestCorner;
return true;
case "farthest-side":
TextGradient.SizeMode = GradientInfo.RadialSizeMode.FarthestSide;
return true;
}
return false;
}
bool SetTextGradientAngle( string value )
{
var angle = GetAngleInDegrees( value );
if ( !angle.HasValue ) return false;
var angler = angle.Value;
TextGradient.Angle = angler.Value;
return true;
}
bool SetFontSmooth( string value )
{
value = value.Trim();
if ( Enum.TryParse<FontSmooth>( value, true, out var fontSmooth ) )
{
FontSmooth = fontSmooth;
return true;
}
return false;
}
bool SetObjectFit( string value )
{
value = value.Trim();
if ( Enum.TryParse<ObjectFit>( value, true, out var objectFit ) )
{
ObjectFit = objectFit;
return true;
}
return false;
}
Length? GetAngleInDegrees( string value )
{
var p = new Parse( value );
p.SkipWhitespaceAndNewlines();
//
// https://www.w3.org/TR/css-images-3/#linear-gradient-syntax
// top/bottom are flipped in order to match css spec, our coordinate systems differ
// from browser implementations
//
Dictionary<string, float> directions = new Dictionary<string, float>()
{
{ "bottom", 0 },
{ "right", 90 },
{ "top", 180 },
{ "left", 270 }
};
Length? result = null;
if ( p.Is( "to ", 0, true ) )
{
p.Pointer += 3;
p.SkipWhitespaceAndNewlines();
foreach ( var (name, angle) in directions )
{
if ( p.Is( name, 0, true ) )
return angle;
}
}
var lastP = p;
// We only want to fetch specific units from here, otherwise we'll fallback to parsing other units
if ( result == null && p.TryReadLength( out var lenx ) )
{
// If it's pixels, lets check the other units
if ( lenx.Unit != LengthUnit.Pixels )
return lenx;
}
// Still not found
if ( result == null )
{
// Reset our pointer and try and parse with units
p = lastP;
if ( p.TryReadFloat( out var num ) )
{
var unit = "deg";
if ( p.IsLetter ) unit = p.ReadUntilWhitespaceOrNewlineOrEnd( "," );
// CSS angles - +x is assumed to be 0 degrees, whereas we would assume +y is 0 degrees,
// so we add 90deg here in order to match the CSS spec.
return StyleHelpers.RotationDegrees( num, unit ) + 90f;
}
}
return null;
}
}
}