diff --git a/packages/core/core-types/index.ts b/packages/core/core-types/index.ts index 52172b49ab..b244998e19 100644 --- a/packages/core/core-types/index.ts +++ b/packages/core/core-types/index.ts @@ -29,8 +29,9 @@ export namespace CoreTypes { export type LengthPxUnit = { readonly unit: 'px'; readonly value: px }; export type LengthPercentUnit = { readonly unit: '%'; readonly value: percent }; - export type LengthType = 'auto' | dip | LengthDipUnit | LengthPxUnit; - export type PercentLengthType = 'auto' | dip | LengthDipUnit | LengthPxUnit | LengthPercentUnit; + export type FixedLengthType = dip | LengthDipUnit | LengthPxUnit; + export type LengthType = 'auto' | FixedLengthType; + export type PercentLengthType = 'auto' | FixedLengthType | LengthPercentUnit; export const zeroLength: LengthType = { value: 0, diff --git a/packages/core/ui/animation/animation-interfaces.ts b/packages/core/ui/animation/animation-interfaces.ts index d86e923c4a..a37c7a603a 100644 --- a/packages/core/ui/animation/animation-interfaces.ts +++ b/packages/core/ui/animation/animation-interfaces.ts @@ -8,13 +8,19 @@ export type Transformation = { value: TransformationValue; }; -export type TransformationType = 'rotate' | 'translate' | 'translateX' | 'translateY' | 'scale' | 'scaleX' | 'scaleY'; +export type TransformationType = 'rotate' | 'rotate3d' | 'rotateX' | 'rotateY' | 'translate' | 'translate3d' | 'translateX' | 'translateY' | 'scale' | 'scale3d' | 'scaleX' | 'scaleY'; -export type TransformationValue = Pair | number; +export type TransformationValue = Point3D | Pair | number; + +export interface Point3D { + x: number; + y: number; + z: number; +} export type TransformFunctionsInfo = { translate: Pair; - rotate: number; + rotate: Point3D; scale: Pair; }; @@ -55,7 +61,7 @@ export interface AnimationDefinition { scale?: Pair; height?: CoreTypes.PercentLengthType | string; width?: CoreTypes.PercentLengthType | string; - rotate?: number; + rotate?: Point3D; duration?: number; delay?: number; iterations?: number; diff --git a/packages/core/ui/animation/index.d.ts b/packages/core/ui/animation/index.d.ts index 0041ba53b5..4eaae4c6cd 100644 --- a/packages/core/ui/animation/index.d.ts +++ b/packages/core/ui/animation/index.d.ts @@ -83,7 +83,7 @@ export type Transformation = { /** * Defines possible css transformations */ -export type TransformationType = 'rotate' | 'rotateX' | 'rotateY' | 'translate' | 'translateX' | 'translateY' | 'scale' | 'scaleX' | 'scaleY'; +export type TransformationType = 'rotate' | 'rotate3d' | 'rotateX' | 'rotateY' | 'translate' | 'translate3d' | 'translateX' | 'translateY' | 'scale' | 'scale3d' | 'scaleX' | 'scaleY'; /** * Defines possible css transformation values diff --git a/packages/core/ui/styling/background-common.ts b/packages/core/ui/styling/background-common.ts index 4d52da76cd..e7fdd3479c 100644 --- a/packages/core/ui/styling/background-common.ts +++ b/packages/core/ui/styling/background-common.ts @@ -3,6 +3,7 @@ import { LinearGradient } from './linear-gradient'; // Types. import { Color } from '../../color'; import { BoxShadow } from './box-shadow'; +import { ClipPathFunction } from './clip-path-function'; /** * Flags used to hint the background handler if it has to clear a specific property @@ -39,7 +40,7 @@ export class Background { public borderTopRightRadius = 0; public borderBottomLeftRadius = 0; public borderBottomRightRadius = 0; - public clipPath: string; + public clipPath: string | ClipPathFunction; public boxShadow: BoxShadow; public clearFlags: number = BackgroundClearFlags.NONE; @@ -192,7 +193,7 @@ export class Background { return clone; } - public withClipPath(value: string): Background { + public withClipPath(value: string | ClipPathFunction): Background { const clone = this.clone(); clone.clipPath = value; @@ -224,16 +225,23 @@ export class Background { return false; } - let imagesEqual = false; + let isImageEqual = false; if (value1 instanceof LinearGradient && value2 instanceof LinearGradient) { - imagesEqual = LinearGradient.equals(value1, value2); + isImageEqual = LinearGradient.equals(value1, value2); } else { - imagesEqual = value1.image === value2.image; + isImageEqual = value1.image === value2.image; + } + + let isClipPathEqual = false; + if (value1.clipPath instanceof ClipPathFunction && value2.clipPath instanceof ClipPathFunction) { + isClipPathEqual = ClipPathFunction.equals(value1.clipPath, value2.clipPath); + } else { + isClipPathEqual = value1.clipPath === value2.clipPath; } return ( Color.equals(value1.color, value2.color) && - imagesEqual && + isImageEqual && value1.position === value2.position && value1.repeat === value2.repeat && value1.size === value2.size && @@ -249,7 +257,7 @@ export class Background { value1.borderTopRightRadius === value2.borderTopRightRadius && value1.borderBottomRightRadius === value2.borderBottomRightRadius && value1.borderBottomLeftRadius === value2.borderBottomLeftRadius && - value1.clipPath === value2.clipPath + isClipPathEqual // && value1.clearFlags === value2.clearFlags ); } diff --git a/packages/core/ui/styling/background.android.ts b/packages/core/ui/styling/background.android.ts index 08885273e8..ec4d0bed55 100644 --- a/packages/core/ui/styling/background.android.ts +++ b/packages/core/ui/styling/background.android.ts @@ -1,5 +1,6 @@ import { View } from '../core/view'; import { LinearGradient } from './linear-gradient'; +import { ClipPathFunction } from './clip-path-function'; import { isDataURI, isFileOrResourcePath, RESOURCE_PREFIX, FILE_PREFIX } from '../../utils'; import { parse } from '../../css-value'; import { path, knownFolders } from '../../file-system'; @@ -92,7 +93,7 @@ export function refreshBorderDrawable(view: View, borderDrawable: org.nativescri background.borderBottomRightRadius, background.borderBottomLeftRadius, - background.clipPath, + background.clipPath instanceof ClipPathFunction ? background.clipPath.toString() : background.clipPath, background.color ? background.color.android : 0, imageUri, @@ -103,7 +104,7 @@ export function refreshBorderDrawable(view: View, borderDrawable: org.nativescri background.position, backgroundPositionParsedCSSValues, background.size, - backgroundSizeParsedCSSValues + backgroundSizeParsedCSSValues, ); //console.log(`>>> ${borderDrawable.toDebugString()}`); } diff --git a/packages/core/ui/styling/background.d.ts b/packages/core/ui/styling/background.d.ts index 934e40a4f3..4adc26080f 100644 --- a/packages/core/ui/styling/background.d.ts +++ b/packages/core/ui/styling/background.d.ts @@ -1,7 +1,8 @@ import { Color } from '../../color'; import { View } from '../core/view'; import { BackgroundRepeat } from '../../css/parser'; -import { LinearGradient } from '../styling/linear-gradient'; +import { LinearGradient } from './linear-gradient'; +import { ClipPathFunction } from './clip-path-function'; import { BoxShadow } from './box-shadow'; import { Background as BackgroundDefinition } from './background-common'; @@ -32,7 +33,7 @@ export enum CacheMode { // public borderTopRightRadius: number; // public borderBottomRightRadius: number; // public borderBottomLeftRadius: number; -// public clipPath: string; +// public clipPath: string | ClipPathFunction; // public boxShadow: string | BoxShadow; // public clearFlags: number; @@ -80,12 +81,12 @@ export namespace ios { export function drawBackgroundVisualEffects(view: View): void; export function clearBackgroundVisualEffects(view: View): void; export function createUIImageFromURI(view: View, imageURI: string, flip: boolean, callback: (image: any) => void): void; - export function generateClipPath(view: View, bounds: CGRect): any; - export function generateShadowLayerPaths(view: View, bounds: CGRect): { maskPath: any; shadowPath: any }; - export function getUniformBorderRadius(view: View, bounds: CGRect): number; - export function generateNonUniformBorderInnerClipRoundedPath(view: View, bounds: CGRect): any; - export function generateNonUniformBorderOuterClipRoundedPath(view: View, bounds: CGRect): any; - export function generateNonUniformMultiColorBorderRoundedPaths(view: View, bounds: CGRect): Array; + export function generateClipPath(view: View, bounds: any /* CGRect */): any; + export function generateShadowLayerPaths(view: View, bounds: any /* CGRect */): { maskPath: any; shadowPath: any }; + export function getUniformBorderRadius(view: View, bounds: any /* CGRect */): number; + export function generateNonUniformBorderInnerClipRoundedPath(view: View, bounds: any /* CGRect */): any; + export function generateNonUniformBorderOuterClipRoundedPath(view: View, bounds: any /* CGRect */): any; + export function generateNonUniformMultiColorBorderRoundedPaths(view: View, bounds: any /* CGRect */): Array; } export namespace ad { diff --git a/packages/core/ui/styling/background.ios.ts b/packages/core/ui/styling/background.ios.ts index 2e49ab31ab..01632991a5 100644 --- a/packages/core/ui/styling/background.ios.ts +++ b/packages/core/ui/styling/background.ios.ts @@ -11,6 +11,7 @@ import { parse as cssParse } from '../../css-value/reworkcss-value.js'; import { BoxShadow } from './box-shadow'; import { Length } from './style-properties'; import { BackgroundClearFlags } from './background-common'; +import { ClipPathFunction } from './clip-path-function'; export * from './background-common'; @@ -303,30 +304,28 @@ export namespace ios { let path: UIBezierPath; const clipPath = background.clipPath; - const functionName: string = clipPath.substring(0, clipPath.indexOf('(')); - const value: string = clipPath.replace(`${functionName}(`, '').replace(')', ''); - - switch (functionName) { - case 'rect': - path = rectPath(value, position); - break; - - case 'inset': - path = insetPath(value, position); - break; - - case 'circle': - path = circlePath(value, position); - break; - - case 'ellipse': - path = ellipsePath(value, position); - break; - - case 'polygon': - path = polygonPath(value, position); - break; + if (clipPath instanceof ClipPathFunction) { + switch (clipPath.shape) { + case 'rect': + path = rectPath(clipPath.rule, position); + break; + case 'inset': + path = insetPath(clipPath.rule, position); + break; + case 'circle': + path = circlePath(clipPath.rule, position); + break; + case 'ellipse': + path = ellipsePath(clipPath.rule, position); + break; + case 'polygon': + path = polygonPath(clipPath.rule, position); + break; + } + } else { + path = null; } + return path; } diff --git a/packages/core/ui/styling/clip-path-function.ts b/packages/core/ui/styling/clip-path-function.ts new file mode 100644 index 0000000000..c351add29d --- /dev/null +++ b/packages/core/ui/styling/clip-path-function.ts @@ -0,0 +1,39 @@ +type ClipPathShape = 'rect' | 'circle' | 'ellipse' | 'polygon' | 'inset'; + +interface IClipPathFunction { + shape: ClipPathShape; + rule: string; +} + +export class ClipPathFunction implements IClipPathFunction { + private readonly _shape: ClipPathShape; + private readonly _rule: string; + + constructor(shape: ClipPathShape, rule: string) { + this._shape = shape; + this._rule = rule; + } + + get shape(): ClipPathShape { + return this._shape; + } + + get rule(): string { + return this._rule; + } + + public static equals(value1: ClipPathFunction, value2: ClipPathFunction): boolean { + return value1.shape === value2.shape && value1.rule === value2.rule; + } + + toJSON(): IClipPathFunction { + return { + shape: this._shape, + rule: this._rule, + }; + } + + toString(): string { + return `${this._shape}(${this._rule})`; + } +} diff --git a/packages/core/ui/styling/css-animation-parser.ts b/packages/core/ui/styling/css-animation-parser.ts index eefcd342d9..f071c738e0 100644 --- a/packages/core/ui/styling/css-animation-parser.ts +++ b/packages/core/ui/styling/css-animation-parser.ts @@ -3,7 +3,7 @@ import { CssAnimationProperty } from '../core/properties'; import { KeyframeAnimationInfo, KeyframeDeclaration, KeyframeInfo, UnparsedKeyframe } from '../animation/keyframe-animation'; import { timeConverter, animationTimingFunctionConverter } from '../styling/converters'; -import { transformConverter } from '../styling/style-properties'; +import { transformConverter } from '../styling/css-transform'; import { cleanupImportantFlags } from './css-utils'; const ANIMATION_PROPERTY_HANDLERS = Object.freeze({ diff --git a/packages/core/ui/styling/css-transform.ts b/packages/core/ui/styling/css-transform.ts new file mode 100644 index 0000000000..0187e6a172 --- /dev/null +++ b/packages/core/ui/styling/css-transform.ts @@ -0,0 +1,133 @@ +import { Pair, Transformation, TransformationType, TransformationValue, TransformFunctionsInfo } from '../animation'; +import { radiansToDegrees } from '../../utils/number-utils'; +import { decompose2DTransformMatrix, getTransformMatrix, matrixArrayToCssMatrix, multiplyAffine2d } from '../../matrix'; +import { hasDuplicates } from '../../utils'; + +type TransformationStyleMap = { + [key: string]: (value: TransformationValue) => Transformation; +}; + +const IDENTITY_TRANSFORMATION = { + translate: { x: 0, y: 0 }, + rotate: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1 }, +}; + +const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g); +const TRANSFORMATIONS = Object.freeze(['rotate', 'rotateX', 'rotateY', 'rotate3d', 'translate', 'translate3d', 'translateX', 'translateY', 'scale', 'scale3d', 'scaleX', 'scaleY']); + +const STYLE_TRANSFORMATION_MAP: TransformationStyleMap = Object.freeze({ + scale: (value: number) => ({ property: 'scale', value }), + scale3d: (value: number) => ({ property: 'scale', value }), + scaleX: ({ x }: Pair) => ({ + property: 'scale', + value: { x, y: IDENTITY_TRANSFORMATION.scale.y }, + }), + scaleY: ({ y }: Pair) => ({ + property: 'scale', + value: { y, x: IDENTITY_TRANSFORMATION.scale.x }, + }), + translate: (value) => ({ property: 'translate', value }), + translate3d: (value) => ({ property: 'translate', value }), + translateX: ({ x }: Pair) => ({ + property: 'translate', + value: { x, y: IDENTITY_TRANSFORMATION.translate.y }, + }), + translateY: ({ y }: Pair) => ({ + property: 'translate', + value: { y, x: IDENTITY_TRANSFORMATION.translate.x }, + }), + + rotate3d: (value) => ({ property: 'rotate', value }), + rotateX: (x: number) => ({ + property: 'rotate', + value: { + x, + y: IDENTITY_TRANSFORMATION.rotate.y, + z: IDENTITY_TRANSFORMATION.rotate.z, + }, + }), + rotateY: (y: number) => ({ + property: 'rotate', + value: { + x: IDENTITY_TRANSFORMATION.rotate.x, + y, + z: IDENTITY_TRANSFORMATION.rotate.z, + }, + }), + rotate: (z: number) => ({ + property: 'rotate', + value: { + x: IDENTITY_TRANSFORMATION.rotate.x, + y: IDENTITY_TRANSFORMATION.rotate.y, + z, + }, + }), +}); + +export function transformConverter(text: string): TransformFunctionsInfo { + const transformations = parseTransformString(text); + + if (text === 'none' || text === '' || !transformations.length) { + return IDENTITY_TRANSFORMATION; + } + + const usedTransforms = transformations.map((t) => t.property); + if (!hasDuplicates(usedTransforms)) { + const fullTransformations = { ...IDENTITY_TRANSFORMATION }; + transformations.forEach((transform) => { + fullTransformations[transform.property] = transform.value; + }); + + return fullTransformations; + } + + const affineMatrix = transformations.map(getTransformMatrix).reduce(multiplyAffine2d); + const cssMatrix = matrixArrayToCssMatrix(affineMatrix); + + return decompose2DTransformMatrix(cssMatrix); +} + +function isTransformType(propertyName: string): propertyName is TransformationType { + return (TRANSFORMATIONS as string[]).indexOf(propertyName) !== -1; +} + +// using general regex and manually checking the matched +// properties is faster than using more specific regex +// https://jsperf.com/cssparse +function parseTransformString(text: string): Transformation[] { + const matches: Transformation[] = []; + let match: RegExpExecArray; + + while ((match = TRANSFORM_SPLITTER.exec(text)) !== null) { + const property = match[1]; + + if (isTransformType(property)) { + const value = convertTransformValue(property, match[2]); + matches.push(STYLE_TRANSFORMATION_MAP[property](value)); + } + } + + return matches; +} + +function convertTransformValue(property: TransformationType, rawValue: string): TransformationValue { + const values = rawValue.split(',').map(parseFloat); + const x = values[0]; + + let y = values[1]; + let z = values[2]; + + if (property === 'translate') { + y ??= IDENTITY_TRANSFORMATION.translate.y; + } else { + y ??= x; + z ??= y; + } + + if (property === 'rotate' || property === 'rotateX' || property === 'rotateY') { + return rawValue.slice(-3) === 'rad' ? radiansToDegrees(x) : x; + } + + return { x, y, z }; +} diff --git a/packages/core/ui/styling/style-properties.d.ts b/packages/core/ui/styling/style-properties.d.ts index 61f99e3d0a..0743bd35b6 100644 --- a/packages/core/ui/styling/style-properties.d.ts +++ b/packages/core/ui/styling/style-properties.d.ts @@ -1,17 +1,29 @@ -import { TransformFunctionsInfo } from '../animation'; import { CoreTypes } from '../../core-types'; import { Color } from '../../color'; import { CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from '../core/properties'; import { Style } from './style'; import { Font, FontStyleType, FontWeightType, FontVariationSettingsType } from './font'; import { Background } from './background'; +import { ClipPathFunction } from './clip-path-function'; +import { LinearGradient } from './linear-gradient'; + +export namespace FixedLength { + export function parse(text: string): CoreTypes.FixedLengthType; + export function equals(a: CoreTypes.FixedLengthType, b: CoreTypes.FixedLengthType): boolean; + /** + * Converts FixedLengthType unit to device pixels. + * @param length The FixedLengthType to convert. + */ + export function toDevicePixels(length: CoreTypes.FixedLengthType): number; + export function convertToString(length: CoreTypes.FixedLengthType): string; +} export namespace Length { export function parse(text: string): CoreTypes.LengthType; export function equals(a: CoreTypes.LengthType, b: CoreTypes.LengthType): boolean; /** - * Converts Length unit to device pixels. - * @param length The Length to convert. + * Converts LengthType unit to device pixels. + * @param length The LengthType to convert. * @param auto Value to use for conversion of "auto". By default is Math.NaN. */ export function toDevicePixels(length: CoreTypes.LengthType, auto?: number): number; @@ -39,14 +51,12 @@ export const scaleYProperty: CssAnimationProperty; export const translateXProperty: CssAnimationProperty; export const translateYProperty: CssAnimationProperty; -export function transformConverter(text: string): TransformFunctionsInfo; - -export const clipPathProperty: CssProperty; +export const clipPathProperty: CssProperty; export const colorProperty: InheritedCssProperty; export const backgroundProperty: ShorthandProperty; export const backgroundColorProperty: CssAnimationProperty; -export const backgroundImageProperty: CssProperty; +export const backgroundImageProperty: CssProperty; export const backgroundRepeatProperty: CssProperty; export const backgroundSizeProperty: CssProperty; export const backgroundPositionProperty: CssProperty; diff --git a/packages/core/ui/styling/style-properties.ts b/packages/core/ui/styling/style-properties.ts index 50a7ad3a7d..6d097189bc 100644 --- a/packages/core/ui/styling/style-properties.ts +++ b/packages/core/ui/styling/style-properties.ts @@ -1,59 +1,59 @@ -// Types import { unsetValue, CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from '../core/properties'; import { Style } from './style'; -import { Transformation, TransformationValue, TransformFunctionsInfo } from '../animation'; import { Color } from '../../color'; import { Font, parseFont, FontStyle, FontStyleType, FontWeight, FontWeightType, FontVariationSettings, FontVariationSettingsType } from './font'; import { Background } from './background'; -import { layout, hasDuplicates } from '../../utils'; +import { layout } from '../../utils'; -import { radiansToDegrees } from '../../utils/number-utils'; - -import { decompose2DTransformMatrix, getTransformMatrix, matrixArrayToCssMatrix, multiplyAffine2d } from '../../matrix'; import { Trace } from '../../trace'; import { CoreTypes } from '../../core-types'; import { parseBackground } from '../../css/parser'; import { LinearGradient } from './linear-gradient'; import { parseCSSShadow, ShadowCSSValues } from './css-shadow'; +import { transformConverter } from './css-transform'; +import { ClipPathFunction } from './clip-path-function'; + +interface ShorthandPositioning { + top: string; + right: string; + bottom: string; + left: string; +} function equalsCommon(a: CoreTypes.LengthType, b: CoreTypes.LengthType): boolean; function equalsCommon(a: CoreTypes.PercentLengthType, b: CoreTypes.PercentLengthType): boolean; function equalsCommon(a: CoreTypes.PercentLengthType, b: CoreTypes.PercentLengthType): boolean { if (a == 'auto') { - // tslint:disable-line - return b == 'auto'; // tslint:disable-line + return b == 'auto'; + } + + if (b == 'auto') { + return false; } + if (typeof a === 'number') { - if (b == 'auto') { - // tslint:disable-line - return false; - } if (typeof b === 'number') { - return a == b; // tslint:disable-line + return a == b; } if (!b) { return false; } - return b.unit == 'dip' && a == b.value; // tslint:disable-line - } - if (b == 'auto') { - // tslint:disable-line - return false; + return b.unit == 'dip' && a == b.value; } + if (typeof b === 'number') { - return a ? a.unit == 'dip' && a.value == b : false; // tslint:disable-line + return a ? a.unit == 'dip' && a.value == b : false; } if (!a || !b) { return false; } - return a.value == b.value && a.unit == b.unit; // tslint:disable-line + return a.value == b.value && a.unit == b.unit; } function convertToStringCommon(length: CoreTypes.LengthType | CoreTypes.PercentLengthType): string { if (length == 'auto') { - // tslint:disable-line return 'auto'; } @@ -71,7 +71,6 @@ function convertToStringCommon(length: CoreTypes.LengthType | CoreTypes.PercentL function toDevicePixelsCommon(length: CoreTypes.PercentLengthType, auto: number = Number.NaN, parentAvailableWidth: number = Number.NaN): number { if (length == 'auto') { - // tslint:disable-line return auto; } if (typeof length === 'number') { @@ -94,9 +93,9 @@ function toDevicePixelsCommon(length: CoreTypes.PercentLengthType, auto: number export namespace PercentLength { export function parse(fromValue: string | CoreTypes.LengthType): CoreTypes.PercentLengthType { if (fromValue == 'auto') { - // tslint:disable-line return 'auto'; } + if (typeof fromValue === 'string') { let stringValue = fromValue.trim(); const percentIndex = stringValue.indexOf('%'); @@ -147,12 +146,8 @@ export namespace PercentLength { } = convertToStringCommon; } -export namespace Length { - export function parse(fromValue: string | CoreTypes.LengthType): CoreTypes.LengthType { - if (fromValue == 'auto') { - // tslint:disable-line - return 'auto'; - } +export namespace FixedLength { + export function parse(fromValue: string | CoreTypes.FixedLengthType): CoreTypes.FixedLengthType { if (typeof fromValue === 'string') { let stringValue = fromValue.trim(); if (stringValue.indexOf('px') !== -1) { @@ -175,6 +170,23 @@ export namespace Length { return fromValue; } } + export const equals: { (a: CoreTypes.FixedLengthType, b: CoreTypes.FixedLengthType): boolean } = equalsCommon; + export const toDevicePixels: { + (length: CoreTypes.FixedLengthType): number; + } = toDevicePixelsCommon; + export const convertToString: { + (length: CoreTypes.FixedLengthType): string; + } = convertToStringCommon; +} + +export namespace Length { + export function parse(fromValue: string | CoreTypes.LengthType): CoreTypes.LengthType { + if (fromValue == 'auto') { + return 'auto'; + } + + return FixedLength.parse(fromValue); + } export const equals: { (a: CoreTypes.LengthType, b: CoreTypes.LengthType): boolean } = equalsCommon; export const toDevicePixels: { (length: CoreTypes.LengthType, auto?: number): number; @@ -184,6 +196,185 @@ export namespace Length { } = convertToStringCommon; } +function isNonNegativeFiniteNumber(value: number): boolean { + return isFinite(value) && !isNaN(value) && value >= 0; +} + +function parseClipPath(value: string): string | ClipPathFunction { + const functionStartIndex = value.indexOf('('); + + if (functionStartIndex > -1) { + const functionName = value.substring(0, functionStartIndex).trim(); + + switch (functionName) { + case 'rect': + case 'circle': + case 'ellipse': + case 'polygon': + case 'inset': { + const rule: string = value.replace(`${functionName}(`, '').replace(')', ''); + return new ClipPathFunction(functionName, rule); + } + default: + throw new Error(`Clip-path function ${functionName} is not valid.`); + } + } else { + if (value === 'none') { + return null; + } + + // Only shape functions and none are supported for now + throw new Error(`Clip-path value ${value} is not valid.`); + } +} + +function parseShorthandPositioning(value: string): ShorthandPositioning { + const arr = value.split(/[ ,]+/); + + let top: string; + let right: string; + let bottom: string; + let left: string; + + if (arr.length === 1) { + top = arr[0]; + right = arr[0]; + bottom = arr[0]; + left = arr[0]; + } else if (arr.length === 2) { + top = arr[0]; + bottom = arr[0]; + right = arr[1]; + left = arr[1]; + } else if (arr.length === 3) { + top = arr[0]; + right = arr[1]; + left = arr[1]; + bottom = arr[2]; + } else if (arr.length === 4) { + top = arr[0]; + right = arr[1]; + bottom = arr[2]; + left = arr[3]; + } else { + throw new Error('Expected 1, 2, 3 or 4 parameters. Actual: ' + value); + } + + return { + top: top, + right: right, + bottom: bottom, + left: left, + }; +} + +function parseBorderColorPositioning(value: string): ShorthandPositioning { + if (value.indexOf('rgb') === 0 || value.indexOf('hsl') === 0) { + return { + top: value, + right: value, + bottom: value, + left: value, + }; + } + + return parseShorthandPositioning(value); +} + +function convertToBackgrounds(value: string): [CssProperty | CssAnimationProperty, any][] { + if (typeof value === 'string') { + const backgrounds = parseBackground(value).value; + let backgroundColor = unsetValue; + if (backgrounds.color) { + backgroundColor = backgrounds.color instanceof Color ? backgrounds.color : new Color(backgrounds.color); + } + + let backgroundImage: string | LinearGradient; + if (typeof backgrounds.image === 'object' && backgrounds.image) { + backgroundImage = LinearGradient.parse(backgrounds.image); + } else { + backgroundImage = backgrounds.image || unsetValue; + } + + const backgroundRepeat = backgrounds.repeat || unsetValue; + const backgroundPosition = backgrounds.position ? backgrounds.position.text : unsetValue; + + return [ + [backgroundColorProperty, backgroundColor], + [backgroundImageProperty, backgroundImage], + [backgroundRepeatProperty, backgroundRepeat], + [backgroundPositionProperty, backgroundPosition], + ]; + } else { + return [ + [backgroundColorProperty, unsetValue], + [backgroundImageProperty, unsetValue], + [backgroundRepeatProperty, unsetValue], + [backgroundPositionProperty, unsetValue], + ]; + } +} + +function convertToMargins(value: string | CoreTypes.PercentLengthType): [CssProperty, CoreTypes.PercentLengthType][] { + if (typeof value === 'string' && value !== 'auto') { + const thickness = parseShorthandPositioning(value); + + return [ + [marginTopProperty, PercentLength.parse(thickness.top)], + [marginRightProperty, PercentLength.parse(thickness.right)], + [marginBottomProperty, PercentLength.parse(thickness.bottom)], + [marginLeftProperty, PercentLength.parse(thickness.left)], + ]; + } else { + return [ + [marginTopProperty, value], + [marginRightProperty, value], + [marginBottomProperty, value], + [marginLeftProperty, value], + ]; + } +} + +function convertToPaddings(value: string | CoreTypes.LengthType): [CssProperty, CoreTypes.LengthType][] { + if (typeof value === 'string' && value !== 'auto') { + const thickness = parseShorthandPositioning(value); + + return [ + [paddingTopProperty, Length.parse(thickness.top)], + [paddingRightProperty, Length.parse(thickness.right)], + [paddingBottomProperty, Length.parse(thickness.bottom)], + [paddingLeftProperty, Length.parse(thickness.left)], + ]; + } else { + return [ + [paddingTopProperty, value], + [paddingRightProperty, value], + [paddingBottomProperty, value], + [paddingLeftProperty, value], + ]; + } +} + +function convertToTransform(value: string): [CssAnimationProperty, any][] { + if (value === unsetValue) { + value = 'none'; + } + + const { translate, rotate, scale } = transformConverter(value); + + return [ + [translateXProperty, translate.x], + [translateYProperty, translate.y], + + [scaleXProperty, scale.x], + [scaleYProperty, scale.y], + + [rotateProperty, rotate.z], + [rotateXProperty, rotate.x], + [rotateYProperty, rotate.y], + ]; +} + export const minWidthProperty = new CssProperty({ name: 'minWidth', cssName: 'min-width', @@ -416,97 +607,6 @@ export const verticalAlignmentProperty = new CssProperty, any][] { - if (typeof value === 'string' && value !== 'auto') { - const thickness = parseThickness(value); - - return [ - [marginTopProperty, PercentLength.parse(thickness.top)], - [marginRightProperty, PercentLength.parse(thickness.right)], - [marginBottomProperty, PercentLength.parse(thickness.bottom)], - [marginLeftProperty, PercentLength.parse(thickness.left)], - ]; - } else { - return [ - [marginTopProperty, value], - [marginRightProperty, value], - [marginBottomProperty, value], - [marginLeftProperty, value], - ]; - } -} - -function convertToPaddings(this: void, value: string | CoreTypes.LengthType): [CssProperty, any][] { - if (typeof value === 'string' && value !== 'auto') { - const thickness = parseThickness(value); - - return [ - [paddingTopProperty, Length.parse(thickness.top)], - [paddingRightProperty, Length.parse(thickness.right)], - [paddingBottomProperty, Length.parse(thickness.bottom)], - [paddingLeftProperty, Length.parse(thickness.left)], - ]; - } else { - return [ - [paddingTopProperty, value], - [paddingRightProperty, value], - [paddingBottomProperty, value], - [paddingLeftProperty, value], - ]; - } -} - export const rotateProperty = new CssAnimationProperty({ name: 'rotate', cssName: 'rotate', @@ -555,27 +655,21 @@ export const scaleYProperty = new CssAnimationProperty({ }); scaleYProperty.register(Style); -function parseDIPs(value: string): CoreTypes.dip { - if (value.indexOf('px') !== -1) { - return layout.toDeviceIndependentPixels(parseFloat(value.replace('px', '').trim())); - } else { - return parseFloat(value.replace('dip', '').trim()); - } -} - -export const translateXProperty = new CssAnimationProperty({ +export const translateXProperty = new CssAnimationProperty({ name: 'translateX', cssName: 'translateX', defaultValue: 0, - valueConverter: parseDIPs, + equalityComparer: FixedLength.equals, + valueConverter: FixedLength.parse, }); translateXProperty.register(Style); -export const translateYProperty = new CssAnimationProperty({ +export const translateYProperty = new CssAnimationProperty({ name: 'translateY', cssName: 'translateY', defaultValue: 0, - valueConverter: parseDIPs, + equalityComparer: FixedLength.equals, + valueConverter: FixedLength.parse, }); translateYProperty.register(Style); @@ -608,148 +702,6 @@ const transformProperty = new ShorthandProperty({ }); transformProperty.register(Style); -const IDENTITY_TRANSFORMATION = { - translate: { x: 0, y: 0 }, - rotate: { x: 0, y: 0, z: 0 }, - scale: { x: 1, y: 1 }, -}; - -const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g); -const TRANSFORMATIONS = Object.freeze(['rotate', 'rotateX', 'rotateY', 'rotate3d', 'translate', 'translate3d', 'translateX', 'translateY', 'scale', 'scale3d', 'scaleX', 'scaleY']); - -const STYLE_TRANSFORMATION_MAP = Object.freeze({ - scale: (value) => ({ property: 'scale', value }), - scale3d: (value) => ({ property: 'scale', value }), - scaleX: ({ x }) => ({ - property: 'scale', - value: { x, y: IDENTITY_TRANSFORMATION.scale.y }, - }), - scaleY: ({ y }) => ({ - property: 'scale', - value: { y, x: IDENTITY_TRANSFORMATION.scale.x }, - }), - - translate: (value) => ({ property: 'translate', value }), - translate3d: (value) => ({ property: 'translate', value }), - translateX: ({ x }) => ({ - property: 'translate', - value: { x, y: IDENTITY_TRANSFORMATION.translate.y }, - }), - translateY: ({ y }) => ({ - property: 'translate', - value: { y, x: IDENTITY_TRANSFORMATION.translate.x }, - }), - - rotate3d: (value) => ({ property: 'rotate', value }), - rotateX: (x) => ({ - property: 'rotate', - value: { - x, - y: IDENTITY_TRANSFORMATION.rotate.y, - z: IDENTITY_TRANSFORMATION.rotate.z, - }, - }), - rotateY: (y) => ({ - property: 'rotate', - value: { - x: IDENTITY_TRANSFORMATION.rotate.x, - y, - z: IDENTITY_TRANSFORMATION.rotate.z, - }, - }), - rotate: (z) => ({ - property: 'rotate', - value: { - x: IDENTITY_TRANSFORMATION.rotate.x, - y: IDENTITY_TRANSFORMATION.rotate.y, - z, - }, - }), -}); - -function convertToTransform(value: string): [CssProperty, any][] { - if (value === unsetValue) { - value = 'none'; - } - - const { translate, rotate, scale } = transformConverter(value); - - return [ - [translateXProperty, translate.x], - [translateYProperty, translate.y], - - [scaleXProperty, scale.x], - [scaleYProperty, scale.y], - - [rotateProperty, rotate.z], - [rotateXProperty, rotate.x], - [rotateYProperty, rotate.y], - ]; -} - -export function transformConverter(text: string): TransformFunctionsInfo { - const transformations = parseTransformString(text); - - if (text === 'none' || text === '' || !transformations.length) { - return IDENTITY_TRANSFORMATION; - } - - const usedTransforms = transformations.map((t) => t.property); - if (!hasDuplicates(usedTransforms)) { - const fullTransformations = { ...IDENTITY_TRANSFORMATION }; - transformations.forEach((transform) => { - fullTransformations[transform.property] = transform.value; - }); - - return fullTransformations; - } - - const affineMatrix = transformations.map(getTransformMatrix).reduce(multiplyAffine2d); - const cssMatrix = matrixArrayToCssMatrix(affineMatrix); - - return decompose2DTransformMatrix(cssMatrix); -} - -// using general regex and manually checking the matched -// properties is faster than using more specific regex -// https://jsperf.com/cssparse -function parseTransformString(text: string): Transformation[] { - const matches: Transformation[] = []; - let match; - - while ((match = TRANSFORM_SPLITTER.exec(text)) !== null) { - const property = match[1]; - const value = convertTransformValue(property, match[2]); - - if (TRANSFORMATIONS.indexOf(property) !== -1) { - matches.push(normalizeTransformation({ property, value })); - } - } - - return matches; -} - -function normalizeTransformation({ property, value }: Transformation): Transformation { - return STYLE_TRANSFORMATION_MAP[property](value); -} - -function convertTransformValue(property: string, stringValue: string): TransformationValue { - // eslint-disable-next-line prefer-const - let [x, y, z] = stringValue.split(',').map(parseFloat); - if (property === 'translate') { - y ??= IDENTITY_TRANSFORMATION.translate.y; - } else { - y ??= x; - z ??= y; - } - - if (property === 'rotate' || property === 'rotateX' || property === 'rotateY') { - return stringValue.slice(-3) === 'rad' ? radiansToDegrees(x) : x; - } - - return { x, y, z }; -} - // Background properties. const backgroundProperty = new ShorthandProperty({ name: 'background', @@ -768,7 +720,6 @@ export const backgroundInternalProperty = new CssProperty({ }); backgroundInternalProperty.register(Style); -// const pattern: RegExp = /url\(('|")(.*?)\1\)/; export const backgroundImageProperty = new CssProperty({ name: 'backgroundImage', cssName: 'background-image', @@ -834,91 +785,6 @@ export const backgroundPositionProperty = new CssProperty({ }); backgroundPositionProperty.register(Style); -function convertToBackgrounds(this: void, value: string): [CssProperty, any][] { - if (typeof value === 'string') { - const backgrounds = parseBackground(value).value; - let backgroundColor = unsetValue; - if (backgrounds.color) { - backgroundColor = backgrounds.color instanceof Color ? backgrounds.color : new Color(backgrounds.color); - } - - let backgroundImage: string | LinearGradient; - if (typeof backgrounds.image === 'object' && backgrounds.image) { - backgroundImage = LinearGradient.parse(backgrounds.image); - } else { - backgroundImage = backgrounds.image || unsetValue; - } - - const backgroundRepeat = backgrounds.repeat || unsetValue; - const backgroundPosition = backgrounds.position ? backgrounds.position.text : unsetValue; - - return [ - [backgroundColorProperty, backgroundColor], - [backgroundImageProperty, backgroundImage], - [backgroundRepeatProperty, backgroundRepeat], - [backgroundPositionProperty, backgroundPosition], - ]; - } else { - return [ - [backgroundColorProperty, unsetValue], - [backgroundImageProperty, unsetValue], - [backgroundRepeatProperty, unsetValue], - [backgroundPositionProperty, unsetValue], - ]; - } -} - -function parseBorderColor(value: string): { top: Color; right: Color; bottom: Color; left: Color } { - const result: { top: Color; right: Color; bottom: Color; left: Color } = { - top: undefined, - right: undefined, - bottom: undefined, - left: undefined, - }; - if (value.indexOf('rgb') === 0 || value.indexOf('hsl') === 0) { - result.top = result.right = result.bottom = result.left = new Color(value); - - return result; - } - - const arr = value.split(/[ ,]+/); - if (arr.length === 1) { - const arr0 = new Color(arr[0]); - result.top = arr0; - result.right = arr0; - result.bottom = arr0; - result.left = arr0; - } else if (arr.length === 2) { - const arr0 = new Color(arr[0]); - const arr1 = new Color(arr[1]); - result.top = arr0; - result.right = arr1; - result.bottom = arr0; - result.left = arr1; - } else if (arr.length === 3) { - const arr0 = new Color(arr[0]); - const arr1 = new Color(arr[1]); - const arr2 = new Color(arr[2]); - result.top = arr0; - result.right = arr1; - result.bottom = arr2; - result.left = arr1; - } else if (arr.length === 4) { - const arr0 = new Color(arr[0]); - const arr1 = new Color(arr[1]); - const arr2 = new Color(arr[2]); - const arr3 = new Color(arr[3]); - result.top = arr0; - result.right = arr1; - result.bottom = arr2; - result.left = arr3; - } else { - throw new Error(`Expected 1, 2, 3 or 4 parameters. Actual: ${value}`); - } - - return result; -} - // Border Color properties. const borderColorProperty = new ShorthandProperty({ name: 'borderColor', @@ -932,13 +798,13 @@ const borderColorProperty = new ShorthandProperty({ }, converter: function (value) { if (typeof value === 'string') { - const fourColors = parseBorderColor(value); + const colors = parseBorderColorPositioning(value); return [ - [borderTopColorProperty, fourColors.top], - [borderRightColorProperty, fourColors.right], - [borderBottomColorProperty, fourColors.bottom], - [borderLeftColorProperty, fourColors.left], + [borderTopColorProperty, new Color(colors.top)], + [borderRightColorProperty, new Color(colors.right)], + [borderBottomColorProperty, new Color(colors.bottom)], + [borderLeftColorProperty, new Color(colors.left)], ]; } else { return [ @@ -1009,13 +875,13 @@ const borderWidthProperty = new ShorthandProperty({ }); boxShadowProperty.register(Style); -function isNonNegativeFiniteNumber(value: number): boolean { - return isFinite(value) && !isNaN(value) && value >= 0; -} - -const supportedPaths = ['rect', 'circle', 'ellipse', 'polygon', 'inset']; -function isClipPathValid(value: string): boolean { - if (!value) { - return true; - } - const functionName = value.substring(0, value.indexOf('(')).trim(); - - return supportedPaths.indexOf(functionName) !== -1; -} - -export const clipPathProperty = new CssProperty({ +export const clipPathProperty = new CssProperty({ name: 'clipPath', cssName: 'clip-path', valueChanged: (target, oldValue, newValue) => { - if (!isClipPathValid(newValue)) { - throw new Error('clip-path is not valid.'); + target.backgroundInternal = target.backgroundInternal.withClipPath(newValue); + }, + equalityComparer: (value1, value2) => { + if (value1 instanceof ClipPathFunction && value2 instanceof ClipPathFunction) { + return ClipPathFunction.equals(value1, value2); + } + return value1 === value2; + }, + valueConverter(value: string | ClipPathFunction) { + if (typeof value === 'string') { + return parseClipPath(value); } - target.backgroundInternal = target.backgroundInternal.withClipPath(newValue); + return value; }, }); clipPathProperty.register(Style); -function isFloatValueConverter(value: string): number { - const newValue = parseFloat(value); - if (isNaN(newValue)) { - throw new Error(`Invalid value: ${newValue}`); - } - - return newValue; -} - export const zIndexProperty = new CssProperty({ name: 'zIndex', cssName: 'z-index', - valueConverter: isFloatValueConverter, -}); -zIndexProperty.register(Style); + valueConverter: (value: string): number => { + const newValue = parseFloat(value); + if (isNaN(newValue)) { + throw new Error(`Invalid value: ${newValue}`); + } -function opacityConverter(value: any): number { - const newValue = parseFloat(value); - if (!isNaN(newValue) && 0 <= newValue && newValue <= 1) { return newValue; - } - - throw new Error(`Opacity should be between [0, 1]. Value: ${newValue}`); -} + }, +}); +zIndexProperty.register(Style); export const opacityProperty = new CssAnimationProperty({ name: 'opacity', cssName: 'opacity', defaultValue: 1, - valueConverter: opacityConverter, + valueConverter: (value: string): number => { + const newValue = parseFloat(value); + if (!isNonNegativeFiniteNumber(newValue) || newValue > 1) { + throw new Error(`Opacity should be between [0, 1]. Value: ${newValue}`); + } + + return newValue; + }, }); opacityProperty.register(Style); diff --git a/packages/core/ui/styling/style/index.ts b/packages/core/ui/styling/style/index.ts index 30839455a6..29bffe392a 100644 --- a/packages/core/ui/styling/style/index.ts +++ b/packages/core/ui/styling/style/index.ts @@ -12,6 +12,7 @@ import { CoreTypes } from '../../../core-types'; import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types'; import { ShadowCSSValues } from '../css-shadow'; import { StrokeCSSValues } from '../css-stroke'; +import { ClipPathFunction } from '../clip-path-function'; export interface CommonLayoutParams { width: number; @@ -122,7 +123,7 @@ export class Style extends Observable implements StyleDefinition { public translateX: CoreTypes.dip; public translateY: CoreTypes.dip; - public clipPath: string; + public clipPath: string | ClipPathFunction; public color: Color; public tintColor: Color; public placeholderColor: Color;