mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-16 02:09:20 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2264 lines
49 KiB
C#
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;
|
|
}
|
|
|
|
}
|
|
}
|