mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
Merge branch 'master'
This commit is contained in:
296
nativescript-core/ui/styling/background-common.ts
Normal file
296
nativescript-core/ui/styling/background-common.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
// Deifinitions.
|
||||
import { Background as BackgroundDefinition } from "./background";
|
||||
import { BackgroundRepeat } from "../core/view";
|
||||
import { LinearGradient } from "./linear-gradient";
|
||||
// Types.
|
||||
import { Color } from "../../color";
|
||||
|
||||
export class Background implements BackgroundDefinition {
|
||||
public static default = new Background();
|
||||
|
||||
public color: Color;
|
||||
public image: string | LinearGradient;
|
||||
public repeat: BackgroundRepeat;
|
||||
public position: string;
|
||||
public size: string;
|
||||
public borderTopColor: Color;
|
||||
public borderRightColor: Color;
|
||||
public borderBottomColor: Color;
|
||||
public borderLeftColor: Color;
|
||||
public borderTopWidth: number = 0;
|
||||
public borderRightWidth: number = 0;
|
||||
public borderBottomWidth: number = 0;
|
||||
public borderLeftWidth: number = 0;
|
||||
public borderTopLeftRadius: number = 0;
|
||||
public borderTopRightRadius: number = 0;
|
||||
public borderBottomLeftRadius: number = 0;
|
||||
public borderBottomRightRadius: number = 0;
|
||||
public clipPath: string;
|
||||
|
||||
private clone(): Background {
|
||||
const clone = new Background();
|
||||
|
||||
clone.color = this.color;
|
||||
clone.image = this.image;
|
||||
clone.repeat = this.repeat;
|
||||
clone.position = this.position;
|
||||
clone.size = this.size;
|
||||
clone.borderTopColor = this.borderTopColor;
|
||||
clone.borderRightColor = this.borderRightColor;
|
||||
clone.borderBottomColor = this.borderBottomColor;
|
||||
clone.borderLeftColor = this.borderLeftColor;
|
||||
clone.borderTopWidth = this.borderTopWidth;
|
||||
clone.borderRightWidth = this.borderRightWidth;
|
||||
clone.borderBottomWidth = this.borderBottomWidth;
|
||||
clone.borderLeftWidth = this.borderLeftWidth;
|
||||
clone.borderTopLeftRadius = this.borderTopLeftRadius;
|
||||
clone.borderTopRightRadius = this.borderTopRightRadius;
|
||||
clone.borderBottomRightRadius = this.borderBottomRightRadius;
|
||||
clone.borderBottomLeftRadius = this.borderBottomLeftRadius;
|
||||
clone.clipPath = this.clipPath;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withColor(value: Color): Background {
|
||||
const clone = this.clone();
|
||||
clone.color = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withImage(value: string | LinearGradient): Background {
|
||||
const clone = this.clone();
|
||||
clone.image = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withRepeat(value: BackgroundRepeat): Background {
|
||||
const clone = this.clone();
|
||||
clone.repeat = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withPosition(value: string): Background {
|
||||
const clone = this.clone();
|
||||
clone.position = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withSize(value: string): Background {
|
||||
const clone = this.clone();
|
||||
clone.size = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderTopColor(value: Color): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderTopColor = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderRightColor(value: Color): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderRightColor = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderBottomColor(value: Color): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderBottomColor = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderLeftColor(value: Color): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderLeftColor = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderTopWidth(value: number): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderTopWidth = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderRightWidth(value: number): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderRightWidth = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderBottomWidth(value: number): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderBottomWidth = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderLeftWidth(value: number): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderLeftWidth = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderTopLeftRadius(value: number): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderTopLeftRadius = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderTopRightRadius(value: number): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderTopRightRadius = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderBottomRightRadius(value: number): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderBottomRightRadius = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withBorderBottomLeftRadius(value: number): Background {
|
||||
const clone = this.clone();
|
||||
clone.borderBottomLeftRadius = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withClipPath(value: string): Background {
|
||||
const clone = this.clone();
|
||||
clone.clipPath = value;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return !this.color
|
||||
&& !this.image
|
||||
&& !this.hasBorderWidth()
|
||||
&& !this.hasBorderRadius()
|
||||
&& !this.clipPath;
|
||||
}
|
||||
|
||||
public static equals(value1: Background, value2: Background): boolean {
|
||||
// both values are falsy
|
||||
if (!value1 && !value2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// only one is falsy
|
||||
if (!value1 || !value2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let imagesEqual = false;
|
||||
if (value1 instanceof LinearGradient && value2 instanceof LinearGradient) {
|
||||
imagesEqual = LinearGradient.equals(value1, value2);
|
||||
} else {
|
||||
imagesEqual = value1.image === value2.image;
|
||||
}
|
||||
|
||||
return Color.equals(value1.color, value2.color)
|
||||
&& imagesEqual
|
||||
&& value1.position === value2.position
|
||||
&& value1.repeat === value2.repeat
|
||||
&& value1.size === value2.size
|
||||
&& Color.equals(value1.borderTopColor, value2.borderTopColor)
|
||||
&& Color.equals(value1.borderRightColor, value2.borderRightColor)
|
||||
&& Color.equals(value1.borderBottomColor, value2.borderBottomColor)
|
||||
&& Color.equals(value1.borderLeftColor, value2.borderLeftColor)
|
||||
&& value1.borderTopWidth === value2.borderTopWidth
|
||||
&& value1.borderRightWidth === value2.borderRightWidth
|
||||
&& value1.borderBottomWidth === value2.borderBottomWidth
|
||||
&& value1.borderLeftWidth === value2.borderLeftWidth
|
||||
&& value1.borderTopLeftRadius === value2.borderTopLeftRadius
|
||||
&& value1.borderTopRightRadius === value2.borderTopRightRadius
|
||||
&& value1.borderBottomRightRadius === value2.borderBottomRightRadius
|
||||
&& value1.borderBottomLeftRadius === value2.borderBottomLeftRadius
|
||||
&& value1.clipPath === value2.clipPath;
|
||||
}
|
||||
|
||||
public hasBorderColor(): boolean {
|
||||
return !!this.borderTopColor || !!this.borderRightColor || !!this.borderBottomColor || !!this.borderLeftColor;
|
||||
}
|
||||
|
||||
public hasBorderWidth(): boolean {
|
||||
return this.borderTopWidth > 0
|
||||
|| this.borderRightWidth > 0
|
||||
|| this.borderBottomWidth > 0
|
||||
|| this.borderLeftWidth > 0;
|
||||
}
|
||||
|
||||
public hasBorderRadius(): boolean {
|
||||
return this.borderTopLeftRadius > 0
|
||||
|| this.borderTopRightRadius > 0
|
||||
|| this.borderBottomRightRadius > 0
|
||||
|| this.borderBottomLeftRadius > 0;
|
||||
}
|
||||
|
||||
public hasUniformBorderColor(): boolean {
|
||||
return Color.equals(this.borderTopColor, this.borderRightColor)
|
||||
&& Color.equals(this.borderTopColor, this.borderBottomColor)
|
||||
&& Color.equals(this.borderTopColor, this.borderLeftColor);
|
||||
}
|
||||
|
||||
public hasUniformBorderWidth(): boolean {
|
||||
return this.borderTopWidth === this.borderRightWidth
|
||||
&& this.borderTopWidth === this.borderBottomWidth
|
||||
&& this.borderTopWidth === this.borderLeftWidth;
|
||||
}
|
||||
|
||||
public hasUniformBorderRadius(): boolean {
|
||||
return this.borderTopLeftRadius === this.borderTopRightRadius
|
||||
&& this.borderTopLeftRadius === this.borderBottomRightRadius
|
||||
&& this.borderTopLeftRadius === this.borderBottomLeftRadius;
|
||||
}
|
||||
|
||||
public hasUniformBorder(): boolean {
|
||||
return this.hasUniformBorderColor()
|
||||
&& this.hasUniformBorderWidth()
|
||||
&& this.hasUniformBorderRadius();
|
||||
}
|
||||
|
||||
public getUniformBorderColor(): Color {
|
||||
if (this.hasUniformBorderColor()) {
|
||||
return this.borderTopColor;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getUniformBorderWidth(): number {
|
||||
if (this.hasUniformBorderWidth()) {
|
||||
return this.borderTopWidth;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public getUniformBorderRadius(): number {
|
||||
if (this.hasUniformBorderRadius()) {
|
||||
return this.borderTopLeftRadius;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `isEmpty: ${this.isEmpty()}; color: ${this.color}; image: ${this.image}; repeat: ${this.repeat}; position: ${this.position}; size: ${this.size}; borderTopColor: ${this.borderTopColor}; borderRightColor: ${this.borderRightColor}; borderBottomColor: ${this.borderBottomColor}; borderLeftColor: ${this.borderLeftColor}; borderTopWidth: ${this.borderTopWidth}; borderRightWidth: ${this.borderRightWidth}; borderBottomWidth: ${this.borderBottomWidth}; borderLeftWidth: ${this.borderLeftWidth}; borderTopLeftRadius: ${this.borderTopLeftRadius}; borderTopRightRadius: ${this.borderTopRightRadius}; borderBottomRightRadius: ${this.borderBottomRightRadius}; borderBottomLeftRadius: ${this.borderBottomLeftRadius}; clipPath: ${this.clipPath};`;
|
||||
}
|
||||
}
|
||||
298
nativescript-core/ui/styling/background.android.ts
Normal file
298
nativescript-core/ui/styling/background.android.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import { View } from "../core/view";
|
||||
import { LinearGradient } from "./linear-gradient";
|
||||
|
||||
import { isDataURI, isFileOrResourcePath, layout, RESOURCE_PREFIX, FILE_PREFIX } from "../../utils/utils";
|
||||
import { parse } from "../../css-value";
|
||||
import { path, knownFolders } from "../../file-system";
|
||||
import * as application from "../../application";
|
||||
import { profile } from "../../profiling";
|
||||
export * from "./background-common";
|
||||
|
||||
interface AndroidView {
|
||||
_cachedDrawable: android.graphics.drawable.Drawable.ConstantState | android.graphics.drawable.Drawable;
|
||||
}
|
||||
|
||||
// TODO: Change this implementation to use
|
||||
// We are using "ad" here to avoid namespace collision with the global android object
|
||||
export module ad {
|
||||
let SDK: number;
|
||||
function getSDK() {
|
||||
if (!SDK) {
|
||||
SDK = android.os.Build.VERSION.SDK_INT;
|
||||
}
|
||||
|
||||
return SDK;
|
||||
}
|
||||
|
||||
function isSetColorFilterOnlyWidget(nativeView: android.view.View): boolean {
|
||||
return (
|
||||
nativeView instanceof android.widget.Button ||
|
||||
(nativeView instanceof androidx.appcompat.widget.Toolbar
|
||||
&& getSDK() >= 21 // There is an issue with the DrawableContainer which was fixed for API version 21 and above: https://code.google.com/p/android/issues/detail?id=60183
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function onBackgroundOrBorderPropertyChanged(view: View) {
|
||||
const nativeView = <android.view.View>view.nativeViewProtected;
|
||||
if (!nativeView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const background = view.style.backgroundInternal;
|
||||
let drawable = nativeView.getBackground();
|
||||
const androidView = <any>view as AndroidView;
|
||||
// use undefined as not set. getBackground will never return undefined only Drawable or null;
|
||||
if (androidView._cachedDrawable === undefined && drawable) {
|
||||
const constantState = drawable.getConstantState();
|
||||
androidView._cachedDrawable = constantState || drawable;
|
||||
}
|
||||
|
||||
if (isSetColorFilterOnlyWidget(nativeView)
|
||||
&& drawable
|
||||
&& !background.hasBorderWidth()
|
||||
&& !background.hasBorderRadius()
|
||||
&& !background.clipPath
|
||||
&& !background.image
|
||||
&& background.color) {
|
||||
|
||||
if (drawable instanceof org.nativescript.widgets.BorderDrawable) {
|
||||
if (!(androidView._cachedDrawable instanceof android.graphics.drawable.Drawable.ConstantState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
drawable = androidView._cachedDrawable.newDrawable(nativeView.getResources());
|
||||
nativeView.setBackground(drawable);
|
||||
}
|
||||
|
||||
const backgroundColor = (<any>drawable).backgroundColor = background.color.android;
|
||||
drawable.mutate();
|
||||
drawable.setColorFilter(backgroundColor, android.graphics.PorterDuff.Mode.SRC_IN);
|
||||
drawable.invalidateSelf(); // Make sure the drawable is invalidated. Android forgets to invalidate it in some cases: toolbar
|
||||
(<any>drawable).backgroundColor = backgroundColor;
|
||||
} else if (!background.isEmpty()) {
|
||||
let backgroundDrawable = drawable as org.nativescript.widgets.BorderDrawable;
|
||||
if (!(drawable instanceof org.nativescript.widgets.BorderDrawable)) {
|
||||
backgroundDrawable = new org.nativescript.widgets.BorderDrawable(layout.getDisplayDensity(), view.toString());
|
||||
refreshBorderDrawable(view, backgroundDrawable);
|
||||
nativeView.setBackground(backgroundDrawable);
|
||||
} else {
|
||||
refreshBorderDrawable(view, backgroundDrawable);
|
||||
}
|
||||
} else {
|
||||
const cachedDrawable = androidView._cachedDrawable;
|
||||
let defaultDrawable: android.graphics.drawable.Drawable;
|
||||
if (cachedDrawable instanceof android.graphics.drawable.Drawable.ConstantState) {
|
||||
defaultDrawable = cachedDrawable.newDrawable(nativeView.getResources());
|
||||
} else if (cachedDrawable instanceof android.graphics.drawable.Drawable) {
|
||||
defaultDrawable = cachedDrawable;
|
||||
} else {
|
||||
defaultDrawable = null;
|
||||
}
|
||||
|
||||
nativeView.setBackground(defaultDrawable);
|
||||
// TODO: Do we need to clear the drawable here? Can't we just reuse it again?
|
||||
androidView._cachedDrawable = undefined;
|
||||
}
|
||||
|
||||
// TODO: Can we move BorderWidths as separate native setter?
|
||||
// This way we could skip setPadding if borderWidth is not changed.
|
||||
const leftPadding = Math.ceil(view.effectiveBorderLeftWidth + view.effectivePaddingLeft);
|
||||
const topPadding = Math.ceil(view.effectiveBorderTopWidth + view.effectivePaddingTop);
|
||||
const rightPadding = Math.ceil(view.effectiveBorderRightWidth + view.effectivePaddingRight);
|
||||
const bottomPadding = Math.ceil(view.effectiveBorderBottomWidth + view.effectivePaddingBottom);
|
||||
|
||||
nativeView.setPadding(
|
||||
leftPadding,
|
||||
topPadding,
|
||||
rightPadding,
|
||||
bottomPadding
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function fromBase64(source: string): android.graphics.Bitmap {
|
||||
const bytes = android.util.Base64.decode(source, android.util.Base64.DEFAULT);
|
||||
|
||||
return android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
function fromGradient(gradient: LinearGradient): org.nativescript.widgets.LinearGradientDefinition {
|
||||
const colors = Array.create("int", gradient.colorStops.length);
|
||||
const stops = Array.create("float", gradient.colorStops.length);
|
||||
let hasStops = false;
|
||||
gradient.colorStops.forEach((stop, index) => {
|
||||
colors[index] = stop.color.android;
|
||||
if (stop.offset) {
|
||||
stops[index] = stop.offset.value;
|
||||
hasStops = true;
|
||||
}
|
||||
});
|
||||
|
||||
const alpha = gradient.angle / (Math.PI * 2);
|
||||
const startX = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.75)),
|
||||
2
|
||||
);
|
||||
const startY = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.5)),
|
||||
2
|
||||
);
|
||||
const endX = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.25)),
|
||||
2
|
||||
);
|
||||
const endY = Math.pow(
|
||||
Math.sin(Math.PI * alpha),
|
||||
2
|
||||
);
|
||||
|
||||
return new org.nativescript.widgets.LinearGradientDefinition(startX, startY, endX, endY, colors, hasStops ? stops : null);
|
||||
}
|
||||
|
||||
const pattern: RegExp = /url\(('|")(.*?)\1\)/;
|
||||
function refreshBorderDrawable(this: void, view: View, borderDrawable: org.nativescript.widgets.BorderDrawable) {
|
||||
const nativeView = <android.view.View>view.nativeViewProtected;
|
||||
const context = nativeView.getContext();
|
||||
|
||||
const background = view.style.backgroundInternal;
|
||||
if (background) {
|
||||
const backgroundPositionParsedCSSValues = createNativeCSSValueArray(background.position);
|
||||
const backgroundSizeParsedCSSValues = createNativeCSSValueArray(background.size);
|
||||
const blackColor = -16777216; //android.graphics.Color.BLACK;
|
||||
|
||||
let imageUri: string;
|
||||
if (background.image && typeof background.image === "string") {
|
||||
imageUri = background.image;
|
||||
const match = imageUri.match(pattern);
|
||||
if (match && match[2]) {
|
||||
imageUri = match[2];
|
||||
}
|
||||
}
|
||||
|
||||
let bitmap: android.graphics.Bitmap = null;
|
||||
if (isDataURI(imageUri)) {
|
||||
const base64Data = imageUri.split(",")[1];
|
||||
if (base64Data !== undefined) {
|
||||
bitmap = fromBase64(base64Data);
|
||||
imageUri = null;
|
||||
}
|
||||
} else if (isFileOrResourcePath(imageUri)) {
|
||||
if (imageUri.indexOf(RESOURCE_PREFIX) !== 0) {
|
||||
let fileName = imageUri;
|
||||
if (fileName.indexOf("~/") === 0) {
|
||||
fileName = path.join(knownFolders.currentApp().path, fileName.replace("~/", ""));
|
||||
}
|
||||
|
||||
imageUri = FILE_PREFIX + fileName;
|
||||
}
|
||||
}
|
||||
|
||||
let gradient: org.nativescript.widgets.LinearGradientDefinition = null;
|
||||
if (background.image && background.image instanceof LinearGradient) {
|
||||
gradient = fromGradient(background.image);
|
||||
}
|
||||
|
||||
borderDrawable.refresh(
|
||||
background.borderTopColor ? background.borderTopColor.android : blackColor,
|
||||
background.borderRightColor ? background.borderRightColor.android : blackColor,
|
||||
background.borderBottomColor ? background.borderBottomColor.android : blackColor,
|
||||
background.borderLeftColor ? background.borderLeftColor.android : blackColor,
|
||||
|
||||
background.borderTopWidth,
|
||||
background.borderRightWidth,
|
||||
background.borderBottomWidth,
|
||||
background.borderLeftWidth,
|
||||
|
||||
background.borderTopLeftRadius,
|
||||
background.borderTopRightRadius,
|
||||
background.borderBottomRightRadius,
|
||||
background.borderBottomLeftRadius,
|
||||
|
||||
background.clipPath,
|
||||
|
||||
background.color ? background.color.android : 0,
|
||||
imageUri,
|
||||
bitmap,
|
||||
gradient,
|
||||
context,
|
||||
background.repeat,
|
||||
background.position,
|
||||
backgroundPositionParsedCSSValues,
|
||||
background.size,
|
||||
backgroundSizeParsedCSSValues
|
||||
);
|
||||
//console.log(`>>> ${borderDrawable.toDebugString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
function createNativeCSSValueArray(css: string): native.Array<org.nativescript.widgets.CSSValue> {
|
||||
if (!css) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cssValues = parse(css);
|
||||
const nativeArray = Array.create(org.nativescript.widgets.CSSValue, cssValues.length);
|
||||
for (let i = 0, length = cssValues.length; i < length; i++) {
|
||||
nativeArray[i] = new org.nativescript.widgets.CSSValue(
|
||||
cssValues[i].type,
|
||||
cssValues[i].string,
|
||||
cssValues[i].unit,
|
||||
cssValues[i].value
|
||||
);
|
||||
}
|
||||
|
||||
return nativeArray;
|
||||
}
|
||||
|
||||
export enum CacheMode {
|
||||
none,
|
||||
memory,
|
||||
diskAndMemory
|
||||
}
|
||||
|
||||
let currentCacheMode: CacheMode;
|
||||
let imageFetcher: org.nativescript.widgets.image.Fetcher;
|
||||
|
||||
export function initImageCache(context: android.content.Context, mode = CacheMode.diskAndMemory, memoryCacheSize: number = 0.25, diskCacheSize: number = 10 * 1024 * 1024): void {
|
||||
if (currentCacheMode === mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentCacheMode = mode;
|
||||
if (!imageFetcher) {
|
||||
imageFetcher = org.nativescript.widgets.image.Fetcher.getInstance(context);
|
||||
} else {
|
||||
imageFetcher.clearCache();
|
||||
}
|
||||
|
||||
const params = new org.nativescript.widgets.image.Cache.CacheParams();
|
||||
params.memoryCacheEnabled = mode !== CacheMode.none;
|
||||
params.setMemCacheSizePercent(memoryCacheSize); // Set memory cache to % of app memory
|
||||
params.diskCacheEnabled = mode === CacheMode.diskAndMemory;
|
||||
params.diskCacheSize = diskCacheSize;
|
||||
const imageCache = org.nativescript.widgets.image.Cache.getInstance(params);
|
||||
imageFetcher.addImageCache(imageCache);
|
||||
imageFetcher.initCache();
|
||||
}
|
||||
|
||||
function onLivesync(args): void {
|
||||
if (imageFetcher) {
|
||||
imageFetcher.clearCache();
|
||||
}
|
||||
}
|
||||
application.on("livesync", onLivesync);
|
||||
|
||||
application.android.on("activityStarted", profile("initImageCache", args => {
|
||||
if (!imageFetcher) {
|
||||
initImageCache(args.activity);
|
||||
} else {
|
||||
imageFetcher.initCache();
|
||||
}
|
||||
}));
|
||||
|
||||
application.android.on("activityStopped", profile("closeImageCache", args => {
|
||||
if (imageFetcher) {
|
||||
imageFetcher.closeCache();
|
||||
}
|
||||
}));
|
||||
70
nativescript-core/ui/styling/background.d.ts
vendored
Normal file
70
nativescript-core/ui/styling/background.d.ts
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @module "ui/styling/background"
|
||||
*/ /** */
|
||||
|
||||
import { Color } from "../../color";
|
||||
import { View, BackgroundRepeat, LinearGradient } from "../core/view";
|
||||
|
||||
export class Background {
|
||||
public static default: Background;
|
||||
public color: Color;
|
||||
public image: string | LinearGradient;
|
||||
public repeat: BackgroundRepeat;
|
||||
public position: string;
|
||||
public size: string;
|
||||
public borderTopColor: Color;
|
||||
public borderRightColor: Color;
|
||||
public borderBottomColor: Color;
|
||||
public borderLeftColor: Color;
|
||||
public borderTopWidth: number;
|
||||
public borderRightWidth: number;
|
||||
public borderBottomWidth: number;
|
||||
public borderLeftWidth: number;
|
||||
public borderTopLeftRadius: number;
|
||||
public borderTopRightRadius: number;
|
||||
public borderBottomRightRadius: number;
|
||||
public borderBottomLeftRadius: number;
|
||||
public clipPath: string;
|
||||
|
||||
public withColor(value: Color): Background;
|
||||
public withImage(value: string | LinearGradient): Background;
|
||||
public withRepeat(value: BackgroundRepeat): Background;
|
||||
public withPosition(value: string): Background;
|
||||
public withSize(value: string): Background;
|
||||
public withBorderTopColor(value: Color): Background;
|
||||
public withBorderRightColor(value: Color): Background;
|
||||
public withBorderBottomColor(value: Color): Background;
|
||||
public withBorderLeftColor(value: Color): Background;
|
||||
public withBorderTopWidth(value: number): Background;
|
||||
public withBorderRightWidth(value: number): Background;
|
||||
public withBorderBottomWidth(value: number): Background;
|
||||
public withBorderLeftWidth(value: number): Background;
|
||||
public withBorderTopLeftRadius(value: number): Background;
|
||||
public withBorderTopRightRadius(value: number): Background;
|
||||
public withBorderBottomRightRadius(value: number): Background;
|
||||
public withBorderBottomLeftRadius(value: number): Background;
|
||||
public withClipPath(value: string): Background;
|
||||
|
||||
public isEmpty(): boolean;
|
||||
|
||||
public static equals(value1: Background, value2: Background): boolean;
|
||||
|
||||
public hasBorderColor(): boolean;
|
||||
public hasBorderWidth(): boolean;
|
||||
public hasBorderRadius(): boolean;
|
||||
public hasUniformBorderColor(): boolean;
|
||||
public hasUniformBorderWidth(): boolean;
|
||||
public hasUniformBorderRadius(): boolean;
|
||||
public hasUniformBorder(): boolean;
|
||||
public getUniformBorderColor(): Color;
|
||||
public getUniformBorderWidth(): number;
|
||||
public getUniformBorderRadius(): number;
|
||||
}
|
||||
|
||||
export module ios {
|
||||
export function createBackgroundUIColor(view: View, callback: (uiColor: any /* UIColor */) => void, flip?: boolean): void;
|
||||
}
|
||||
|
||||
export module ad {
|
||||
export function onBackgroundOrBorderPropertyChanged(v: View);
|
||||
}
|
||||
891
nativescript-core/ui/styling/background.ios.ts
Normal file
891
nativescript-core/ui/styling/background.ios.ts
Normal file
@@ -0,0 +1,891 @@
|
||||
import { ScrollEventData } from "../scroll-view";
|
||||
|
||||
import { Background as BackgroundDefinition } from "./background";
|
||||
import { View, Point } from "../core/view";
|
||||
import { LinearGradient } from "./linear-gradient";
|
||||
import { Color } from "../../color";
|
||||
import { isDataURI, isFileOrResourcePath, layout } from "../../utils/utils";
|
||||
import { ImageSource } from "../../image-source";
|
||||
import { CSSValue, parse as cssParse } from "../../css-value";
|
||||
|
||||
export * from "./background-common";
|
||||
|
||||
interface NativeView extends UIView {
|
||||
hasNonUniformBorder: boolean;
|
||||
|
||||
borderLayer: CALayer;
|
||||
|
||||
hasBorderMask: boolean;
|
||||
borderOriginalMask: CALayer;
|
||||
|
||||
topBorderLayer: CALayer;
|
||||
rightBorderLayer: CALayer;
|
||||
bottomBorderLayer: CALayer;
|
||||
leftBorderLayer: CALayer;
|
||||
|
||||
gradientLayer: CAGradientLayer;
|
||||
}
|
||||
|
||||
interface Rect {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
const clearCGColor = UIColor.clearColor.CGColor;
|
||||
const symbolUrl = Symbol("backgroundImageUrl");
|
||||
|
||||
export module ios {
|
||||
export function createBackgroundUIColor(view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void {
|
||||
const background = view.style.backgroundInternal;
|
||||
const nativeView = <NativeView>view.nativeViewProtected;
|
||||
|
||||
if (nativeView.hasNonUniformBorder) {
|
||||
unsubscribeFromScrollNotifications(view);
|
||||
clearNonUniformBorders(nativeView);
|
||||
}
|
||||
|
||||
clearGradient(nativeView);
|
||||
if (background.image instanceof LinearGradient) {
|
||||
drawGradient(nativeView, background.image);
|
||||
}
|
||||
|
||||
const hasNonUniformBorderWidths = background.hasBorderWidth() && !background.hasUniformBorder();
|
||||
const hasNonUniformBorderRadiuses = background.hasBorderRadius() && !background.hasUniformBorderRadius();
|
||||
if (background.hasUniformBorderColor() && (hasNonUniformBorderWidths || hasNonUniformBorderRadiuses)) {
|
||||
drawUniformColorNonUniformBorders(nativeView, background);
|
||||
subscribeForScrollNotifications(view);
|
||||
} else if (background.hasUniformBorder()) {
|
||||
const layer = nativeView.layer;
|
||||
const borderColor = background.getUniformBorderColor();
|
||||
layer.borderColor = !borderColor ? undefined : borderColor.ios.CGColor;
|
||||
layer.borderWidth = layout.toDeviceIndependentPixels(background.getUniformBorderWidth());
|
||||
const renderSize = view.getActualSize() || { width: 0, height: 0 };
|
||||
const cornerRadius = layout.toDeviceIndependentPixels(background.getUniformBorderRadius());
|
||||
layer.cornerRadius = Math.min(Math.min(renderSize.width / 2, renderSize.height / 2), cornerRadius);
|
||||
} else {
|
||||
drawNoRadiusNonUniformBorders(nativeView, background);
|
||||
subscribeForScrollNotifications(view);
|
||||
}
|
||||
|
||||
// Clip-path should be called after borders are applied.
|
||||
// It will eventually move them to different layer if uniform.
|
||||
if (background.clipPath) {
|
||||
drawClipPath(nativeView, background);
|
||||
}
|
||||
|
||||
if (!background.image || background.image instanceof LinearGradient) {
|
||||
const uiColor = background.color ? background.color.ios : undefined;
|
||||
callback(uiColor);
|
||||
} else {
|
||||
setUIColorFromImage(view, nativeView, callback, flip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onScroll(this: void, args: ScrollEventData): void {
|
||||
const view = <View>args.object;
|
||||
const nativeView = view.nativeViewProtected;
|
||||
if (nativeView instanceof UIScrollView) {
|
||||
adjustLayersForScrollView(<any>nativeView);
|
||||
}
|
||||
}
|
||||
|
||||
function adjustLayersForScrollView(nativeView: UIScrollView & NativeView) {
|
||||
const layer = nativeView.borderLayer;
|
||||
if (layer instanceof CALayer) {
|
||||
// Compensates with transition for the background layers for scrolling in ScrollView based controls.
|
||||
CATransaction.begin();
|
||||
CATransaction.setValueForKey(kCFBooleanTrue, kCATransactionDisableActions);
|
||||
const offset = nativeView.contentOffset;
|
||||
const transform = { a: 1, b: 0, c: 0, d: 1, tx: offset.x, ty: offset.y };
|
||||
layer.setAffineTransform(transform);
|
||||
if (nativeView.layer.mask) {
|
||||
nativeView.layer.mask.setAffineTransform(transform);
|
||||
}
|
||||
CATransaction.commit();
|
||||
}
|
||||
}
|
||||
|
||||
function unsubscribeFromScrollNotifications(view: View) {
|
||||
if (view.nativeViewProtected instanceof UIScrollView) {
|
||||
view.off("scroll", onScroll);
|
||||
}
|
||||
}
|
||||
function subscribeForScrollNotifications(view: View) {
|
||||
if (view.nativeViewProtected instanceof UIScrollView) {
|
||||
view.on("scroll", onScroll);
|
||||
adjustLayersForScrollView(<any>view.nativeViewProtected);
|
||||
}
|
||||
}
|
||||
|
||||
function clearNonUniformBorders(nativeView: NativeView): void {
|
||||
if (nativeView.borderLayer) {
|
||||
nativeView.borderLayer.removeFromSuperlayer();
|
||||
}
|
||||
|
||||
if (nativeView.hasBorderMask) {
|
||||
nativeView.layer.mask = nativeView.borderOriginalMask;
|
||||
nativeView.hasBorderMask = false;
|
||||
nativeView.borderOriginalMask = null;
|
||||
}
|
||||
|
||||
if (nativeView.topBorderLayer) {
|
||||
nativeView.topBorderLayer.removeFromSuperlayer();
|
||||
}
|
||||
|
||||
if (nativeView.rightBorderLayer) {
|
||||
nativeView.rightBorderLayer.removeFromSuperlayer();
|
||||
}
|
||||
|
||||
if (nativeView.bottomBorderLayer) {
|
||||
nativeView.bottomBorderLayer.removeFromSuperlayer();
|
||||
}
|
||||
|
||||
if (nativeView.leftBorderLayer) {
|
||||
nativeView.leftBorderLayer.removeFromSuperlayer();
|
||||
}
|
||||
}
|
||||
|
||||
const pattern: RegExp = /url\(('|")(.*?)\1\)/;
|
||||
function setUIColorFromImage(view: View, nativeView: UIView, callback: (uiColor: UIColor) => void, flip?: boolean): void {
|
||||
const frame = nativeView.frame;
|
||||
const boundsWidth = view.scaleX ? frame.size.width / view.scaleX : frame.size.width;
|
||||
const boundsHeight = view.scaleY ? frame.size.height / view.scaleY : frame.size.height;
|
||||
if (!boundsWidth || !boundsHeight) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const style = view.style;
|
||||
const background = style.backgroundInternal;
|
||||
let imageUri = background.image as string;
|
||||
if (imageUri) {
|
||||
const match = imageUri.match(pattern);
|
||||
if (match && match[2]) {
|
||||
imageUri = match[2];
|
||||
}
|
||||
}
|
||||
|
||||
let bitmap: UIImage;
|
||||
if (isDataURI(imageUri)) {
|
||||
const base64Data = imageUri.split(",")[1];
|
||||
if (base64Data !== undefined) {
|
||||
const imageSource = ImageSource.fromBase64Sync(base64Data);
|
||||
bitmap = imageSource && imageSource.ios;
|
||||
}
|
||||
} else if (isFileOrResourcePath(imageUri)) {
|
||||
const imageSource = ImageSource.fromFileOrResourceSync(imageUri);
|
||||
bitmap = imageSource && imageSource.ios;
|
||||
} else if (imageUri.indexOf("http") !== -1) {
|
||||
style[symbolUrl] = imageUri;
|
||||
ImageSource.fromUrl(imageUri).then((r) => {
|
||||
if (style && style[symbolUrl] === imageUri) {
|
||||
uiColorFromImage(r.ios, view, callback, flip);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
uiColorFromImage(bitmap, view, callback, flip);
|
||||
}
|
||||
|
||||
interface BackgroundDrawParams {
|
||||
repeatX: boolean;
|
||||
repeatY: boolean;
|
||||
posX: number;
|
||||
posY: number;
|
||||
sizeX?: number;
|
||||
sizeY?: number;
|
||||
}
|
||||
|
||||
function parsePosition(pos: string): { x: CSSValue, y: CSSValue } {
|
||||
const values = cssParse(pos);
|
||||
if (values.length === 2) {
|
||||
return { x: values[0], y: values[1] };
|
||||
}
|
||||
|
||||
if (values.length === 1 && values[0].type === "ident") {
|
||||
const val = values[0].string.toLocaleLowerCase();
|
||||
const center = { type: "ident", string: "center" };
|
||||
|
||||
// If you only one keyword is specified, the other value is "center"
|
||||
if (val === "left" || val === "right") {
|
||||
return { x: values[0], y: center };
|
||||
} else if (val === "top" || val === "bottom") {
|
||||
return { x: center, y: values[0] };
|
||||
} else if (val === "center") {
|
||||
return { x: center, y: center };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDrawParams(this: void, image: UIImage, background: BackgroundDefinition, width: number, height: number): BackgroundDrawParams {
|
||||
if (!image) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const res: BackgroundDrawParams = {
|
||||
repeatX: true,
|
||||
repeatY: true,
|
||||
posX: 0,
|
||||
posY: 0,
|
||||
};
|
||||
|
||||
// repeat
|
||||
if (background.repeat) {
|
||||
switch (background.repeat.toLowerCase()) {
|
||||
case "no-repeat":
|
||||
res.repeatX = false;
|
||||
res.repeatY = false;
|
||||
break;
|
||||
|
||||
case "repeat-x":
|
||||
res.repeatY = false;
|
||||
break;
|
||||
|
||||
case "repeat-y":
|
||||
res.repeatX = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const imageSize = image.size;
|
||||
let imageWidth = imageSize.width;
|
||||
let imageHeight = imageSize.height;
|
||||
|
||||
// size
|
||||
const size = background.size;
|
||||
if (size) {
|
||||
const values = cssParse(size);
|
||||
if (values.length === 2) {
|
||||
const vx = values[0];
|
||||
const vy = values[1];
|
||||
if (vx.unit === "%" && vy.unit === "%") {
|
||||
imageWidth = width * vx.value / 100;
|
||||
imageHeight = height * vy.value / 100;
|
||||
|
||||
res.sizeX = imageWidth;
|
||||
res.sizeY = imageHeight;
|
||||
} else if (vx.type === "number" && vy.type === "number" &&
|
||||
((vx.unit === "px" && vy.unit === "px") || (vx.unit === "" && vy.unit === ""))) {
|
||||
imageWidth = vx.value;
|
||||
imageHeight = vy.value;
|
||||
|
||||
res.sizeX = imageWidth;
|
||||
res.sizeY = imageHeight;
|
||||
}
|
||||
}
|
||||
else if (values.length === 1 && values[0].type === "ident") {
|
||||
let scale = 0;
|
||||
if (values[0].string === "cover") {
|
||||
scale = Math.max(width / imageWidth, height / imageHeight);
|
||||
} else if (values[0].string === "contain") {
|
||||
scale = Math.min(width / imageWidth, height / imageHeight);
|
||||
}
|
||||
|
||||
if (scale > 0) {
|
||||
imageWidth *= scale;
|
||||
imageHeight *= scale;
|
||||
|
||||
res.sizeX = imageWidth;
|
||||
res.sizeY = imageHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// position
|
||||
const position = background.position;
|
||||
if (position) {
|
||||
const v = parsePosition(position);
|
||||
if (v) {
|
||||
const spaceX = width - imageWidth;
|
||||
const spaceY = height - imageHeight;
|
||||
|
||||
if (v.x.unit === "%" && v.y.unit === "%") {
|
||||
res.posX = spaceX * v.x.value / 100;
|
||||
res.posY = spaceY * v.y.value / 100;
|
||||
} else if (v.x.type === "number" && v.y.type === "number" &&
|
||||
((v.x.unit === "px" && v.y.unit === "px") || (v.x.unit === "" && v.y.unit === ""))) {
|
||||
res.posX = v.x.value;
|
||||
res.posY = v.y.value;
|
||||
} else if (v.x.type === "ident" && v.y.type === "ident") {
|
||||
if (v.x.string.toLowerCase() === "center") {
|
||||
res.posX = spaceX / 2;
|
||||
} else if (v.x.string.toLowerCase() === "right") {
|
||||
res.posX = spaceX;
|
||||
}
|
||||
|
||||
if (v.y.string.toLowerCase() === "center") {
|
||||
res.posY = spaceY / 2;
|
||||
} else if (v.y.string.toLowerCase() === "bottom") {
|
||||
res.posY = spaceY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function uiColorFromImage(img: UIImage, view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void {
|
||||
const background = view.style.backgroundInternal;
|
||||
|
||||
if (!img) {
|
||||
callback(background.color && background.color.ios);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const nativeView = view.nativeViewProtected as UIView;
|
||||
const frame = nativeView.frame;
|
||||
const boundsWidth = view.scaleX ? frame.size.width / view.scaleX : frame.size.width;
|
||||
const boundsHeight = view.scaleY ? frame.size.height / view.scaleY : frame.size.height;
|
||||
|
||||
const params = getDrawParams(img, background, boundsWidth, boundsHeight);
|
||||
|
||||
if (params.sizeX > 0 && params.sizeY > 0) {
|
||||
const resizeRect = CGRectMake(0, 0, params.sizeX, params.sizeY);
|
||||
UIGraphicsBeginImageContextWithOptions(resizeRect.size, false, 0.0);
|
||||
img.drawInRect(resizeRect);
|
||||
img = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
}
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeFromString(`{${boundsWidth},${boundsHeight}}`), false, 0.0);
|
||||
const context = UIGraphicsGetCurrentContext();
|
||||
|
||||
if (background.color && background.color.ios) {
|
||||
CGContextSetFillColorWithColor(context, background.color.ios.CGColor);
|
||||
CGContextFillRect(context, CGRectMake(0, 0, boundsWidth, boundsHeight));
|
||||
}
|
||||
|
||||
if (!params.repeatX && !params.repeatY) {
|
||||
img.drawAtPoint(CGPointMake(params.posX, params.posY));
|
||||
} else {
|
||||
const w = params.repeatX ? boundsWidth : img.size.width;
|
||||
const h = params.repeatY ? boundsHeight : img.size.height;
|
||||
|
||||
CGContextSetPatternPhase(context, CGSizeMake(params.posX, params.posY));
|
||||
|
||||
params.posX = params.repeatX ? 0 : params.posX;
|
||||
params.posY = params.repeatY ? 0 : params.posY;
|
||||
|
||||
const patternRect = CGRectMake(params.posX, params.posY, w, h);
|
||||
|
||||
img.drawAsPatternInRect(patternRect);
|
||||
}
|
||||
|
||||
const bkgImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
if (flip) {
|
||||
const flippedImage = _flipImage(bkgImage);
|
||||
callback(UIColor.alloc().initWithPatternImage(flippedImage));
|
||||
} else {
|
||||
callback(UIColor.alloc().initWithPatternImage(bkgImage));
|
||||
}
|
||||
}
|
||||
|
||||
// Flipping the default coordinate system
|
||||
// https://developer.apple.com/library/ios/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/GraphicsDrawingOverview/GraphicsDrawingOverview.html
|
||||
function _flipImage(originalImage: UIImage): UIImage {
|
||||
UIGraphicsBeginImageContextWithOptions(originalImage.size, false, 0.0);
|
||||
const context = UIGraphicsGetCurrentContext();
|
||||
CGContextSaveGState(context);
|
||||
CGContextTranslateCTM(context, 0.0, originalImage.size.height);
|
||||
CGContextScaleCTM(context, 1.0, -1.0);
|
||||
originalImage.drawInRect(CGRectMake(0, 0, originalImage.size.width, originalImage.size.height));
|
||||
CGContextRestoreGState(context);
|
||||
const flippedImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
return flippedImage;
|
||||
}
|
||||
|
||||
function cssValueToDeviceIndependentPixels(source: string, total: number): number {
|
||||
source = source.trim();
|
||||
if (source.indexOf("px") !== -1) {
|
||||
return layout.toDeviceIndependentPixels(parseFloat(source.replace("px", "")));
|
||||
} else if (source.indexOf("%") !== -1 && total > 0) {
|
||||
return (parseFloat(source.replace("%", "")) / 100) * total;
|
||||
} else {
|
||||
return parseFloat(source);
|
||||
}
|
||||
}
|
||||
|
||||
function drawUniformColorNonUniformBorders(nativeView: NativeView, background: BackgroundDefinition) {
|
||||
const layer = nativeView.layer;
|
||||
layer.backgroundColor = undefined;
|
||||
layer.borderColor = undefined;
|
||||
layer.borderWidth = 0;
|
||||
layer.cornerRadius = 0;
|
||||
|
||||
const { width, height } = layer.bounds.size;
|
||||
const { x, y } = layer.bounds.origin;
|
||||
|
||||
const left = x;
|
||||
const top = y;
|
||||
const right = x + width;
|
||||
const bottom = y + height;
|
||||
|
||||
const { min, max } = Math;
|
||||
|
||||
const borderTopWidth = max(0, layout.toDeviceIndependentPixels(background.borderTopWidth));
|
||||
const borderRightWidth = max(0, layout.toDeviceIndependentPixels(background.borderRightWidth));
|
||||
const borderBottomWidth = max(0, layout.toDeviceIndependentPixels(background.borderBottomWidth));
|
||||
const borderLeftWidth = max(0, layout.toDeviceIndependentPixels(background.borderLeftWidth));
|
||||
|
||||
const borderVWidth = borderTopWidth + borderBottomWidth;
|
||||
const borderHWidth = borderLeftWidth + borderRightWidth;
|
||||
|
||||
const cappedBorderTopWidth = borderTopWidth && borderTopWidth * min(1, height / borderVWidth);
|
||||
const cappedBorderRightWidth = borderRightWidth && borderRightWidth * min(1, width / borderHWidth);
|
||||
const cappedBorderBottomWidth = borderBottomWidth && borderBottomWidth * min(1, height / borderVWidth);
|
||||
const cappedBorderLeftWidth = borderLeftWidth && borderLeftWidth * min(1, width / borderHWidth);
|
||||
|
||||
const outerTopLeftRadius = layout.toDeviceIndependentPixels(background.borderTopLeftRadius);
|
||||
const outerTopRightRadius = layout.toDeviceIndependentPixels(background.borderTopRightRadius);
|
||||
const outerBottomRightRadius = layout.toDeviceIndependentPixels(background.borderBottomRightRadius);
|
||||
const outerBottomLeftRadius = layout.toDeviceIndependentPixels(background.borderBottomLeftRadius);
|
||||
|
||||
const topRadii = outerTopLeftRadius + outerTopRightRadius;
|
||||
const rightRadii = outerTopRightRadius + outerBottomRightRadius;
|
||||
const bottomRadii = outerBottomRightRadius + outerBottomLeftRadius;
|
||||
const leftRadii = outerBottomLeftRadius + outerTopLeftRadius;
|
||||
|
||||
function capRadius(a: number, b: number, c: number): number {
|
||||
return a && Math.min(a, Math.min(b, c));
|
||||
}
|
||||
|
||||
const cappedOuterTopLeftRadius = capRadius(outerTopLeftRadius, outerTopLeftRadius / topRadii * width, outerTopLeftRadius / leftRadii * height);
|
||||
const cappedOuterTopRightRadius = capRadius(outerTopRightRadius, outerTopRightRadius / topRadii * width, outerTopRightRadius / rightRadii * height);
|
||||
const cappedOuterBottomRightRadius = capRadius(outerBottomRightRadius, outerBottomRightRadius / bottomRadii * width, outerBottomRightRadius / rightRadii * height);
|
||||
const cappedOuterBottomLeftRadius = capRadius(outerBottomLeftRadius, outerBottomLeftRadius / bottomRadii * width, outerBottomLeftRadius / leftRadii * height);
|
||||
|
||||
// Outer contour
|
||||
const clipPath = CGPathCreateMutable();
|
||||
CGPathMoveToPoint(clipPath, null, left + cappedOuterTopLeftRadius, top);
|
||||
CGPathAddArcToPoint(clipPath, null, right, top, right, top + cappedOuterTopRightRadius, cappedOuterTopRightRadius);
|
||||
CGPathAddArcToPoint(clipPath, null, right, bottom, right - cappedOuterBottomRightRadius, bottom, cappedOuterBottomRightRadius);
|
||||
CGPathAddArcToPoint(clipPath, null, left, bottom, left, bottom - cappedOuterBottomLeftRadius, cappedOuterBottomLeftRadius);
|
||||
CGPathAddArcToPoint(clipPath, null, left, top, left + cappedOuterTopLeftRadius, top, cappedOuterTopLeftRadius);
|
||||
CGPathCloseSubpath(clipPath);
|
||||
|
||||
nativeView.borderOriginalMask = layer.mask;
|
||||
const clipShapeLayer = CAShapeLayer.layer();
|
||||
clipShapeLayer.path = clipPath;
|
||||
layer.mask = clipShapeLayer;
|
||||
nativeView.hasBorderMask = true;
|
||||
|
||||
if (cappedBorderLeftWidth > 0 || cappedBorderTopWidth > 0 || cappedBorderRightWidth > 0 || cappedBorderBottomWidth > 0) {
|
||||
const borderPath = CGPathCreateMutable();
|
||||
CGPathAddRect(borderPath, null, CGRectMake(left, top, width, height));
|
||||
|
||||
// Inner contour
|
||||
if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
|
||||
CGPathMoveToPoint(borderPath, null, left + cappedOuterTopLeftRadius, top + cappedBorderTopWidth);
|
||||
} else {
|
||||
CGPathMoveToPoint(borderPath, null, left, top);
|
||||
}
|
||||
|
||||
if (cappedBorderTopWidth > 0 || cappedBorderRightWidth > 0) {
|
||||
const innerTopRightWRadius = max(0, cappedOuterTopRightRadius - cappedBorderRightWidth);
|
||||
const innerTopRightHRadius = max(0, cappedOuterTopRightRadius - cappedBorderTopWidth);
|
||||
const innerTopRightMaxRadius = max(innerTopRightWRadius, innerTopRightHRadius);
|
||||
const innerTopRightTransform: any = CGAffineTransformMake(
|
||||
innerTopRightMaxRadius && innerTopRightWRadius / innerTopRightMaxRadius, 0,
|
||||
0, innerTopRightMaxRadius && innerTopRightHRadius / innerTopRightMaxRadius,
|
||||
right - cappedBorderRightWidth - innerTopRightWRadius, top + cappedBorderTopWidth + innerTopRightHRadius
|
||||
);
|
||||
CGPathAddArc(borderPath, innerTopRightTransform, 0, 0, innerTopRightMaxRadius, Math.PI * 3 / 2, 0, false);
|
||||
} else {
|
||||
CGPathMoveToPoint(borderPath, null, right, top);
|
||||
}
|
||||
|
||||
if (cappedBorderBottomWidth > 0 || cappedBorderRightWidth > 0) {
|
||||
const innerBottomRightWRadius = max(0, cappedOuterBottomRightRadius - cappedBorderRightWidth);
|
||||
const innerBottomRightHRadius = max(0, cappedOuterBottomRightRadius - cappedBorderBottomWidth);
|
||||
const innerBottomRightMaxRadius = max(innerBottomRightWRadius, innerBottomRightHRadius);
|
||||
const innerBottomRightTransform: any = CGAffineTransformMake(
|
||||
innerBottomRightMaxRadius && innerBottomRightWRadius / innerBottomRightMaxRadius, 0,
|
||||
0, innerBottomRightMaxRadius && innerBottomRightHRadius / innerBottomRightMaxRadius,
|
||||
right - cappedBorderRightWidth - innerBottomRightWRadius, bottom - cappedBorderBottomWidth - innerBottomRightHRadius
|
||||
);
|
||||
CGPathAddArc(borderPath, innerBottomRightTransform, 0, 0, innerBottomRightMaxRadius, 0, Math.PI / 2, false);
|
||||
} else {
|
||||
CGPathAddLineToPoint(borderPath, null, right, bottom);
|
||||
}
|
||||
|
||||
if (cappedBorderBottomWidth > 0 || cappedBorderLeftWidth > 0) {
|
||||
const innerBottomLeftWRadius = max(0, cappedOuterBottomLeftRadius - cappedBorderLeftWidth);
|
||||
const innerBottomLeftHRadius = max(0, cappedOuterBottomLeftRadius - cappedBorderBottomWidth);
|
||||
const innerBottomLeftMaxRadius = max(innerBottomLeftWRadius, innerBottomLeftHRadius);
|
||||
const innerBottomLeftTransform: any = CGAffineTransformMake(
|
||||
innerBottomLeftMaxRadius && innerBottomLeftWRadius / innerBottomLeftMaxRadius, 0,
|
||||
0, innerBottomLeftMaxRadius && innerBottomLeftHRadius / innerBottomLeftMaxRadius,
|
||||
left + cappedBorderLeftWidth + innerBottomLeftWRadius, bottom - cappedBorderBottomWidth - innerBottomLeftHRadius
|
||||
);
|
||||
CGPathAddArc(borderPath, innerBottomLeftTransform, 0, 0, innerBottomLeftMaxRadius, Math.PI / 2, Math.PI, false);
|
||||
} else {
|
||||
CGPathAddLineToPoint(borderPath, null, left, bottom);
|
||||
}
|
||||
|
||||
if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
|
||||
const innerTopLeftWRadius = max(0, cappedOuterTopLeftRadius - cappedBorderLeftWidth);
|
||||
const innerTopLeftHRadius = max(0, cappedOuterTopLeftRadius - cappedBorderTopWidth);
|
||||
const innerTopLeftMaxRadius = max(innerTopLeftWRadius, innerTopLeftHRadius);
|
||||
const innerTopLeftTransform: any = CGAffineTransformMake(
|
||||
innerTopLeftMaxRadius && innerTopLeftWRadius / innerTopLeftMaxRadius, 0,
|
||||
0, innerTopLeftMaxRadius && innerTopLeftHRadius / innerTopLeftMaxRadius,
|
||||
left + cappedBorderLeftWidth + innerTopLeftWRadius, top + cappedBorderTopWidth + innerTopLeftHRadius
|
||||
);
|
||||
CGPathAddArc(borderPath, innerTopLeftTransform, 0, 0, innerTopLeftMaxRadius, Math.PI, Math.PI * 3 / 2, false);
|
||||
} else {
|
||||
CGPathAddLineToPoint(borderPath, null, left, top);
|
||||
}
|
||||
|
||||
CGPathCloseSubpath(borderPath);
|
||||
|
||||
const borderLayer = CAShapeLayer.layer();
|
||||
borderLayer.fillColor = background.borderTopColor && background.borderTopColor.ios.CGColor || UIColor.blackColor.CGColor;
|
||||
borderLayer.fillRule = kCAFillRuleEvenOdd;
|
||||
borderLayer.path = borderPath;
|
||||
layer.addSublayer(borderLayer);
|
||||
nativeView.borderLayer = borderLayer;
|
||||
}
|
||||
|
||||
nativeView.hasNonUniformBorder = true;
|
||||
}
|
||||
|
||||
function drawNoRadiusNonUniformBorders(nativeView: NativeView, background: BackgroundDefinition) {
|
||||
const borderLayer = CALayer.layer();
|
||||
nativeView.layer.addSublayer(borderLayer);
|
||||
nativeView.borderLayer = borderLayer;
|
||||
|
||||
borderLayer.borderColor = undefined;
|
||||
borderLayer.borderWidth = 0;
|
||||
borderLayer.cornerRadius = 0;
|
||||
|
||||
const layerBounds = nativeView.layer.bounds;
|
||||
const layerOrigin = layerBounds.origin;
|
||||
const layerSize = layerBounds.size;
|
||||
|
||||
const nativeViewLayerBounds = {
|
||||
left: layerOrigin.x,
|
||||
top: layerOrigin.y,
|
||||
bottom: layerSize.height,
|
||||
right: layerSize.width
|
||||
};
|
||||
|
||||
const top = layout.toDeviceIndependentPixels(background.borderTopWidth);
|
||||
const right = layout.toDeviceIndependentPixels(background.borderRightWidth);
|
||||
const bottom = layout.toDeviceIndependentPixels(background.borderBottomWidth);
|
||||
const left = layout.toDeviceIndependentPixels(background.borderLeftWidth);
|
||||
|
||||
const lto: Point = { x: nativeViewLayerBounds.left, y: nativeViewLayerBounds.top }; // left-top-outside
|
||||
const lti: Point = { x: nativeViewLayerBounds.left + left, y: nativeViewLayerBounds.top + top }; // left-top-inside
|
||||
|
||||
const rto: Point = { x: nativeViewLayerBounds.right, y: nativeViewLayerBounds.top }; // right-top-outside
|
||||
const rti: Point = { x: nativeViewLayerBounds.right - right, y: nativeViewLayerBounds.top + top }; // right-top-inside
|
||||
|
||||
const rbo: Point = { x: nativeViewLayerBounds.right, y: nativeViewLayerBounds.bottom }; // right-bottom-outside
|
||||
const rbi: Point = { x: nativeViewLayerBounds.right - right, y: nativeViewLayerBounds.bottom - bottom }; // right-bottom-inside
|
||||
|
||||
const lbo: Point = { x: nativeViewLayerBounds.left, y: nativeViewLayerBounds.bottom }; // left-bottom-outside
|
||||
const lbi: Point = { x: nativeViewLayerBounds.left + left, y: nativeViewLayerBounds.bottom - bottom }; // left-bottom-inside
|
||||
|
||||
let hasNonUniformBorder: boolean;
|
||||
|
||||
const borderTopColor = background.borderTopColor;
|
||||
if (top > 0 && borderTopColor && borderTopColor.ios) {
|
||||
const topBorderPath = CGPathCreateMutable();
|
||||
CGPathMoveToPoint(topBorderPath, null, lto.x, lto.y);
|
||||
CGPathAddLineToPoint(topBorderPath, null, rto.x, rto.y);
|
||||
CGPathAddLineToPoint(topBorderPath, null, rti.x, rti.y);
|
||||
CGPathAddLineToPoint(topBorderPath, null, lti.x, lti.y);
|
||||
CGPathAddLineToPoint(topBorderPath, null, lto.x, lto.y);
|
||||
|
||||
const topBorderLayer = CAShapeLayer.layer();
|
||||
topBorderLayer.fillColor = background.borderTopColor.ios.CGColor;
|
||||
topBorderLayer.path = topBorderPath;
|
||||
|
||||
borderLayer.addSublayer(topBorderLayer);
|
||||
nativeView.topBorderLayer = topBorderLayer;
|
||||
hasNonUniformBorder = true;
|
||||
}
|
||||
|
||||
const borderRightColor = background.borderRightColor;
|
||||
if (right > 0 && borderRightColor && borderRightColor.ios) {
|
||||
const rightBorderPath = CGPathCreateMutable();
|
||||
CGPathMoveToPoint(rightBorderPath, null, rto.x, rto.y);
|
||||
CGPathAddLineToPoint(rightBorderPath, null, rbo.x, rbo.y);
|
||||
CGPathAddLineToPoint(rightBorderPath, null, rbi.x, rbi.y);
|
||||
CGPathAddLineToPoint(rightBorderPath, null, rti.x, rti.y);
|
||||
CGPathAddLineToPoint(rightBorderPath, null, rto.x, rto.y);
|
||||
|
||||
const rightBorderLayer = CAShapeLayer.layer();
|
||||
rightBorderLayer.fillColor = background.borderRightColor.ios.CGColor;
|
||||
rightBorderLayer.path = rightBorderPath;
|
||||
|
||||
borderLayer.addSublayer(rightBorderLayer);
|
||||
nativeView.rightBorderLayer = rightBorderLayer;
|
||||
hasNonUniformBorder = true;
|
||||
}
|
||||
|
||||
const borderBottomColor = background.borderBottomColor;
|
||||
if (bottom > 0 && borderBottomColor && borderBottomColor.ios) {
|
||||
const bottomBorderPath = CGPathCreateMutable();
|
||||
CGPathMoveToPoint(bottomBorderPath, null, rbo.x, rbo.y);
|
||||
CGPathAddLineToPoint(bottomBorderPath, null, lbo.x, lbo.y);
|
||||
CGPathAddLineToPoint(bottomBorderPath, null, lbi.x, lbi.y);
|
||||
CGPathAddLineToPoint(bottomBorderPath, null, rbi.x, rbi.y);
|
||||
CGPathAddLineToPoint(bottomBorderPath, null, rbo.x, rbo.y);
|
||||
|
||||
const bottomBorderLayer = CAShapeLayer.layer();
|
||||
bottomBorderLayer.fillColor = background.borderBottomColor.ios.CGColor;
|
||||
bottomBorderLayer.path = bottomBorderPath;
|
||||
|
||||
borderLayer.addSublayer(bottomBorderLayer);
|
||||
nativeView.bottomBorderLayer = bottomBorderLayer;
|
||||
hasNonUniformBorder = true;
|
||||
}
|
||||
|
||||
const borderLeftColor = background.borderLeftColor;
|
||||
if (left > 0 && borderLeftColor && borderLeftColor.ios) {
|
||||
const leftBorderPath = CGPathCreateMutable();
|
||||
CGPathMoveToPoint(leftBorderPath, null, lbo.x, lbo.y);
|
||||
CGPathAddLineToPoint(leftBorderPath, null, lto.x, lto.y);
|
||||
CGPathAddLineToPoint(leftBorderPath, null, lti.x, lti.y);
|
||||
CGPathAddLineToPoint(leftBorderPath, null, lbi.x, lbi.y);
|
||||
CGPathAddLineToPoint(leftBorderPath, null, lbo.x, lbo.y);
|
||||
|
||||
const leftBorderLayer = CAShapeLayer.layer();
|
||||
leftBorderLayer.fillColor = background.borderLeftColor.ios.CGColor;
|
||||
leftBorderLayer.path = leftBorderPath;
|
||||
|
||||
borderLayer.addSublayer(leftBorderLayer);
|
||||
nativeView.leftBorderLayer = leftBorderLayer;
|
||||
hasNonUniformBorder = true;
|
||||
}
|
||||
|
||||
nativeView.hasNonUniformBorder = hasNonUniformBorder;
|
||||
}
|
||||
|
||||
function drawGradient(nativeView: NativeView, gradient: LinearGradient) {
|
||||
|
||||
const gradientLayer = CAGradientLayer.layer();
|
||||
gradientLayer.frame = nativeView.bounds;
|
||||
nativeView.gradientLayer = gradientLayer;
|
||||
|
||||
const iosColors = NSMutableArray.alloc().initWithCapacity(gradient.colorStops.length);
|
||||
const iosStops = NSMutableArray.alloc<number>().initWithCapacity(gradient.colorStops.length);
|
||||
let hasStops = false;
|
||||
|
||||
gradient.colorStops.forEach(stop => {
|
||||
iosColors.addObject(stop.color.ios.CGColor);
|
||||
if (stop.offset) {
|
||||
iosStops.addObject(stop.offset.value);
|
||||
hasStops = true;
|
||||
}
|
||||
});
|
||||
|
||||
gradientLayer.colors = iosColors;
|
||||
|
||||
if (hasStops) {
|
||||
gradientLayer.locations = iosStops;
|
||||
}
|
||||
|
||||
const alpha = gradient.angle / (Math.PI * 2);
|
||||
const startX = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.75)),
|
||||
2
|
||||
);
|
||||
const startY = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.5)),
|
||||
2
|
||||
);
|
||||
const endX = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.25)),
|
||||
2
|
||||
);
|
||||
const endY = Math.pow(
|
||||
Math.sin(Math.PI * alpha),
|
||||
2
|
||||
);
|
||||
gradientLayer.startPoint = { x: startX, y: startY };
|
||||
gradientLayer.endPoint = { x: endX, y: endY };
|
||||
|
||||
nativeView.layer.insertSublayerAtIndex(gradientLayer, 0);
|
||||
|
||||
}
|
||||
|
||||
function clearGradient(nativeView: NativeView): void {
|
||||
if (nativeView.gradientLayer) {
|
||||
nativeView.gradientLayer.removeFromSuperlayer();
|
||||
}
|
||||
}
|
||||
|
||||
function drawClipPath(nativeView: UIView, background: BackgroundDefinition) {
|
||||
const layer = nativeView.layer;
|
||||
const layerBounds = layer.bounds;
|
||||
const layerOrigin = layerBounds.origin;
|
||||
const layerSize = layerBounds.size;
|
||||
|
||||
const bounds = {
|
||||
left: layerOrigin.x,
|
||||
top: layerOrigin.y,
|
||||
bottom: layerSize.height,
|
||||
right: layerSize.width
|
||||
};
|
||||
|
||||
if (bounds.right === 0 || bounds.bottom === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let path: UIBezierPath;
|
||||
const clipPath = background.clipPath;
|
||||
|
||||
const functionName = clipPath.substring(0, clipPath.indexOf("("));
|
||||
const value = clipPath.replace(`${functionName}(`, "").replace(")", "");
|
||||
|
||||
switch (functionName) {
|
||||
case "rect":
|
||||
path = rectPath(value, bounds);
|
||||
break;
|
||||
|
||||
case "inset":
|
||||
path = insetPath(value, bounds);
|
||||
break;
|
||||
|
||||
case "circle":
|
||||
path = circlePath(value, bounds);
|
||||
break;
|
||||
|
||||
case "ellipse":
|
||||
path = ellipsePath(value, bounds);
|
||||
break;
|
||||
|
||||
case "polygon":
|
||||
path = polygonPath(value, bounds);
|
||||
break;
|
||||
}
|
||||
|
||||
if (path) {
|
||||
const shape = CAShapeLayer.layer();
|
||||
shape.path = path;
|
||||
layer.mask = shape;
|
||||
nativeView.clipsToBounds = true;
|
||||
|
||||
const borderWidth = background.getUniformBorderWidth();
|
||||
const borderColor = background.getUniformBorderColor();
|
||||
|
||||
if (borderWidth > 0 && borderColor instanceof Color) {
|
||||
const borderLayer = CAShapeLayer.layer();
|
||||
borderLayer.path = path;
|
||||
borderLayer.lineWidth = borderWidth * 2;
|
||||
borderLayer.strokeColor = borderColor.ios.CGColor;
|
||||
borderLayer.fillColor = clearCGColor;
|
||||
borderLayer.frame = nativeView.bounds;
|
||||
|
||||
layer.borderColor = undefined;
|
||||
layer.borderWidth = 0;
|
||||
layer.addSublayer(borderLayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rectPath(value: string, bounds: Rect): UIBezierPath {
|
||||
const arr = value.split(/[\s]+/);
|
||||
const top = cssValueToDeviceIndependentPixels(arr[0], bounds.top);
|
||||
const right = cssValueToDeviceIndependentPixels(arr[1], bounds.right);
|
||||
const bottom = cssValueToDeviceIndependentPixels(arr[2], bounds.bottom);
|
||||
const left = cssValueToDeviceIndependentPixels(arr[3], bounds.left);
|
||||
|
||||
return UIBezierPath.bezierPathWithRect(CGRectMake(left, top, right - left, bottom - top)).CGPath;
|
||||
}
|
||||
|
||||
function insetPath(value: string, bounds: Rect): UIBezierPath {
|
||||
const arr = value.split(/[\s]+/);
|
||||
|
||||
let topString: string;
|
||||
let rightString: string;
|
||||
let bottomString: string;
|
||||
let leftString: string;
|
||||
if (arr.length === 1) {
|
||||
topString = rightString = bottomString = leftString = arr[0];
|
||||
}
|
||||
else if (arr.length === 2) {
|
||||
topString = bottomString = arr[0];
|
||||
rightString = leftString = arr[1];
|
||||
}
|
||||
else if (arr.length === 3) {
|
||||
topString = arr[0];
|
||||
rightString = leftString = arr[1];
|
||||
bottomString = arr[2];
|
||||
}
|
||||
else if (arr.length === 4) {
|
||||
topString = arr[0];
|
||||
rightString = arr[1];
|
||||
bottomString = arr[2];
|
||||
leftString = arr[3];
|
||||
}
|
||||
|
||||
const top = cssValueToDeviceIndependentPixels(topString, bounds.bottom);
|
||||
const right = cssValueToDeviceIndependentPixels("100%", bounds.right) - cssValueToDeviceIndependentPixels(rightString, bounds.right);
|
||||
const bottom = cssValueToDeviceIndependentPixels("100%", bounds.bottom) - cssValueToDeviceIndependentPixels(bottomString, bounds.bottom);
|
||||
const left = cssValueToDeviceIndependentPixels(leftString, bounds.right);
|
||||
|
||||
return UIBezierPath.bezierPathWithRect(CGRectMake(left, top, right - left, bottom - top)).CGPath;
|
||||
}
|
||||
|
||||
function circlePath(value: string, bounds: Rect): UIBezierPath {
|
||||
const arr = value.split(/[\s]+/);
|
||||
const radius = cssValueToDeviceIndependentPixels(arr[0], (bounds.right > bounds.bottom ? bounds.bottom : bounds.right) / 2);
|
||||
const y = cssValueToDeviceIndependentPixels(arr[2], bounds.bottom);
|
||||
const x = cssValueToDeviceIndependentPixels(arr[3], bounds.right);
|
||||
|
||||
return UIBezierPath.bezierPathWithArcCenterRadiusStartAngleEndAngleClockwise(CGPointMake(x, y), radius, 0, 360, true).CGPath;
|
||||
}
|
||||
|
||||
function ellipsePath(value: string, bounds: Rect): UIBezierPath {
|
||||
const arr = value.split(/[\s]+/);
|
||||
|
||||
const rX = cssValueToDeviceIndependentPixels(arr[0], bounds.right);
|
||||
const rY = cssValueToDeviceIndependentPixels(arr[1], bounds.bottom);
|
||||
const cX = cssValueToDeviceIndependentPixels(arr[3], bounds.right);
|
||||
const cY = cssValueToDeviceIndependentPixels(arr[4], bounds.bottom);
|
||||
|
||||
const left = cX - rX;
|
||||
const top = cY - rY;
|
||||
const width = rX * 2;
|
||||
const height = rY * 2;
|
||||
|
||||
return UIBezierPath.bezierPathWithOvalInRect(CGRectMake(left, top, width, height)).CGPath;
|
||||
}
|
||||
|
||||
function polygonPath(value: string, bounds: Rect): UIBezierPath {
|
||||
const path = CGPathCreateMutable();
|
||||
|
||||
let firstPoint: Point;
|
||||
const arr = value.split(/[,]+/);
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const xy = arr[i].trim().split(/[\s]+/);
|
||||
const point: Point = {
|
||||
x: cssValueToDeviceIndependentPixels(xy[0], bounds.right),
|
||||
y: cssValueToDeviceIndependentPixels(xy[1], bounds.bottom)
|
||||
};
|
||||
|
||||
if (!firstPoint) {
|
||||
firstPoint = point;
|
||||
CGPathMoveToPoint(path, null, point.x, point.y);
|
||||
}
|
||||
|
||||
CGPathAddLineToPoint(path, null, point.x, point.y);
|
||||
}
|
||||
|
||||
CGPathAddLineToPoint(path, null, firstPoint.x, firstPoint.y);
|
||||
|
||||
return path;
|
||||
}
|
||||
8
nativescript-core/ui/styling/converters.d.ts
vendored
Normal file
8
nativescript-core/ui/styling/converters.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @module "ui/styling/converters"
|
||||
*/ /** */
|
||||
|
||||
import { AnimationCurve } from "../enums/enums";
|
||||
|
||||
export function timeConverter(value: string): number;
|
||||
export function animationTimingFunctionConverter(value: string): any;
|
||||
54
nativescript-core/ui/styling/converters.ts
Normal file
54
nativescript-core/ui/styling/converters.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { AnimationCurve } from "../enums";
|
||||
|
||||
const STYLE_CURVE_MAP = Object.freeze({
|
||||
"ease": AnimationCurve.ease,
|
||||
"linear": AnimationCurve.linear,
|
||||
"ease-in": AnimationCurve.easeIn,
|
||||
"ease-out": AnimationCurve.easeOut,
|
||||
"ease-in-out": AnimationCurve.easeInOut,
|
||||
"spring": AnimationCurve.spring,
|
||||
});
|
||||
|
||||
export function timeConverter(value: string): number {
|
||||
let result = parseFloat(value);
|
||||
if (value.indexOf("ms") === -1) {
|
||||
result = result * 1000;
|
||||
}
|
||||
|
||||
return Math.max(0.0, result);
|
||||
}
|
||||
|
||||
export function animationTimingFunctionConverter(value: string): any {
|
||||
return value ?
|
||||
STYLE_CURVE_MAP[value] || parseCubicBezierCurve(value) :
|
||||
AnimationCurve.ease;
|
||||
}
|
||||
|
||||
function parseCubicBezierCurve(value: string) {
|
||||
const coordsString = /\((.*?)\)/.exec(value);
|
||||
const coords = coordsString && coordsString[1]
|
||||
.split(",")
|
||||
.map(stringToBezieCoords);
|
||||
|
||||
if (value.startsWith("cubic-bezier") &&
|
||||
coordsString &&
|
||||
coords.length === 4) {
|
||||
|
||||
const [x1, x2, y1, y2] = [...coords];
|
||||
|
||||
return AnimationCurve.cubicBezier(x1, x2, y1, y2);
|
||||
} else {
|
||||
throw new Error(`Invalid value for animation: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
function stringToBezieCoords(value: string): number {
|
||||
let result = parseFloat(value);
|
||||
if (result < 0) {
|
||||
return 0;
|
||||
} else if (result > 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
21
nativescript-core/ui/styling/css-animation-parser.d.ts
vendored
Normal file
21
nativescript-core/ui/styling/css-animation-parser.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @module "ui/styling/css-animations-parser"
|
||||
*/ /** */
|
||||
|
||||
import {
|
||||
KeyframeAnimationInfo,
|
||||
KeyframeDeclaration,
|
||||
KeyframeInfo,
|
||||
UnparsedKeyframe,
|
||||
} from "../animation/keyframe-animation";
|
||||
import { timeConverter, animationTimingFunctionConverter } from "./converters";
|
||||
|
||||
import { transformConverter } from "./style-properties";
|
||||
|
||||
export class CssAnimationParser {
|
||||
public static keyframeAnimationsFromCSSDeclarations(declarations: KeyframeDeclaration[]): KeyframeAnimationInfo[];
|
||||
|
||||
public static keyframesArrayFromCSS(keyframes: UnparsedKeyframe[]): KeyframeInfo[];
|
||||
}
|
||||
|
||||
export function parseKeyframeDeclarations(unparsedKeyframeDeclarations: KeyframeDeclaration[]) : KeyframeDeclaration[];
|
||||
147
nativescript-core/ui/styling/css-animation-parser.ts
Normal file
147
nativescript-core/ui/styling/css-animation-parser.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
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";
|
||||
|
||||
const ANIMATION_PROPERTY_HANDLERS = Object.freeze({
|
||||
"animation-name": (info, value) => info.name = value,
|
||||
"animation-duration": (info, value) => info.duration = timeConverter(value),
|
||||
"animation-delay": (info, value) => info.delay = timeConverter(value),
|
||||
"animation-timing-function": (info, value) => info.curve = animationTimingFunctionConverter(value),
|
||||
"animation-iteration-count": (info, value) => info.iterations = value === "infinite" ? Number.MAX_VALUE : parseFloat(value),
|
||||
"animation-direction": (info, value) => info.isReverse = value === "reverse",
|
||||
"animation-fill-mode": (info, value) => info.isForwards = value === "forwards"
|
||||
});
|
||||
|
||||
export class CssAnimationParser {
|
||||
public static keyframeAnimationsFromCSSDeclarations(declarations: KeyframeDeclaration[])
|
||||
: KeyframeAnimationInfo[] {
|
||||
|
||||
if (declarations === null || declarations === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let animations = new Array<KeyframeAnimationInfo>();
|
||||
let animationInfo: KeyframeAnimationInfo = undefined;
|
||||
|
||||
declarations.forEach(({ property, value }) => {
|
||||
if (property === "animation") {
|
||||
keyframeAnimationsFromCSSProperty(value, animations);
|
||||
} else {
|
||||
const propertyHandler = ANIMATION_PROPERTY_HANDLERS[property];
|
||||
if (propertyHandler) {
|
||||
if (animationInfo === undefined) {
|
||||
animationInfo = new KeyframeAnimationInfo();
|
||||
animations.push(animationInfo);
|
||||
}
|
||||
propertyHandler(animationInfo, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return animations.length === 0 ? undefined : animations;
|
||||
}
|
||||
|
||||
public static keyframesArrayFromCSS(keyframes: UnparsedKeyframe[]): KeyframeInfo[] {
|
||||
let parsedKeyframes = new Array<KeyframeInfo>();
|
||||
for (let keyframe of keyframes) {
|
||||
let declarations = parseKeyframeDeclarations(keyframe.declarations);
|
||||
for (let time of keyframe.values) {
|
||||
if (time === "from") {
|
||||
time = 0;
|
||||
} else if (time === "to") {
|
||||
time = 1;
|
||||
} else {
|
||||
time = parseFloat(time) / 100;
|
||||
if (time < 0) {
|
||||
time = 0;
|
||||
}
|
||||
if (time > 100) {
|
||||
time = 100;
|
||||
}
|
||||
}
|
||||
let current = parsedKeyframes[time];
|
||||
if (current === undefined) {
|
||||
current = <KeyframeInfo>{};
|
||||
current.duration = time;
|
||||
parsedKeyframes[time] = current;
|
||||
}
|
||||
for (let declaration of keyframe.declarations) {
|
||||
if (declaration.property === "animation-timing-function") {
|
||||
current.curve = animationTimingFunctionConverter(declaration.value);
|
||||
}
|
||||
}
|
||||
current.declarations = declarations;
|
||||
}
|
||||
}
|
||||
let array = new Array();
|
||||
for (let parsedKeyframe in parsedKeyframes) {
|
||||
array.push(parsedKeyframes[parsedKeyframe]);
|
||||
}
|
||||
array.sort(function (a, b) { return a.duration - b.duration; });
|
||||
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
function keyframeAnimationsFromCSSProperty(value: any, animations: KeyframeAnimationInfo[]) {
|
||||
if (typeof value === "string") {
|
||||
let values = value.split(/[,]+/);
|
||||
for (let parsedValue of values) {
|
||||
let animationInfo = new KeyframeAnimationInfo();
|
||||
let arr = (<string>parsedValue).trim().split(/[ ]+/);
|
||||
|
||||
if (arr.length > 0) {
|
||||
animationInfo.name = arr[0];
|
||||
}
|
||||
if (arr.length > 1) {
|
||||
animationInfo.duration = timeConverter(arr[1]);
|
||||
}
|
||||
if (arr.length > 2) {
|
||||
animationInfo.curve = animationTimingFunctionConverter(arr[2]);
|
||||
}
|
||||
if (arr.length > 3) {
|
||||
animationInfo.delay = timeConverter(arr[3]);
|
||||
}
|
||||
if (arr.length > 4) {
|
||||
animationInfo.iterations = parseInt(arr[4]);
|
||||
}
|
||||
if (arr.length > 5) {
|
||||
animationInfo.isReverse = arr[4] === "reverse";
|
||||
}
|
||||
if (arr.length > 6) {
|
||||
animationInfo.isForwards = arr[5] === "forwards";
|
||||
}
|
||||
if (arr.length > 7) {
|
||||
throw new Error("Invalid value for animation: " + value);
|
||||
}
|
||||
animations.push(animationInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseKeyframeDeclarations(unparsedKeyframeDeclarations: KeyframeDeclaration[]): KeyframeDeclaration[] {
|
||||
|
||||
const declarations = unparsedKeyframeDeclarations
|
||||
.reduce((declarations, { property: unparsedProperty, value: unparsedValue }) => {
|
||||
const property = CssAnimationProperty._getByCssName(unparsedProperty);
|
||||
|
||||
if (typeof unparsedProperty === "string" && property && property._valueConverter) {
|
||||
declarations[property.name] = property._valueConverter(<string>unparsedValue);
|
||||
} else if (typeof unparsedValue === "string" && unparsedProperty === "transform") {
|
||||
const transformations = transformConverter(unparsedValue);
|
||||
Object.assign(declarations, transformations);
|
||||
}
|
||||
|
||||
return declarations;
|
||||
}, {});
|
||||
|
||||
return Object.keys(declarations).map(property => ({ property, value: declarations[property] }));
|
||||
}
|
||||
83
nativescript-core/ui/styling/css-selector/css-selector.d.ts
vendored
Normal file
83
nativescript-core/ui/styling/css-selector/css-selector.d.ts
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @module "ui/styling/css-selector"
|
||||
*/ /** */
|
||||
|
||||
import { Node as ParserNode } from "../../../css";
|
||||
|
||||
/**
|
||||
* An interface describing the shape of a type on which the selectors may apply.
|
||||
* Note, the ui/core/view.View implements Node.
|
||||
*/
|
||||
export interface Node {
|
||||
parent?: Node;
|
||||
|
||||
id?: string;
|
||||
cssType?: string;
|
||||
cssClasses?: Set<string>;
|
||||
cssPseudoClasses?: Set<string>;
|
||||
getChildIndex?(node: Node): number
|
||||
getChildAt?(index: number): Node
|
||||
}
|
||||
|
||||
export interface Declaration {
|
||||
property: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SelectorCore {
|
||||
/**
|
||||
* Dynamic selectors depend on attributes and pseudo classes.
|
||||
*/
|
||||
dynamic: boolean;
|
||||
match(node: Node): boolean;
|
||||
ruleset: RuleSet;
|
||||
}
|
||||
|
||||
export class RuleSet {
|
||||
/**
|
||||
* Gets the selectors in this ruleset's selector group.
|
||||
*/
|
||||
selectors: SelectorCore[];
|
||||
|
||||
/**
|
||||
* Gets the key-value list of declarations for the ruleset.
|
||||
*/
|
||||
declarations: Declaration[];
|
||||
|
||||
/**
|
||||
* Optional Tag for rules
|
||||
**/
|
||||
tag: string | Number;
|
||||
}
|
||||
|
||||
export class SelectorsMap {
|
||||
constructor(rules: RuleSet[]);
|
||||
|
||||
/**
|
||||
* Get a list of selectors that are likely to match the node.
|
||||
*/
|
||||
query<T extends Node>(node: T): SelectorsMatch<T>;
|
||||
}
|
||||
|
||||
export type ChangeMap<T extends Node> = Map<T, Changes>;
|
||||
|
||||
export interface Changes {
|
||||
attributes?: Set<string>;
|
||||
pseudoClasses?: Set<string>;
|
||||
}
|
||||
|
||||
export class SelectorsMatch<T extends Node> {
|
||||
/**
|
||||
* Gets the static selectors that match the queried node and the dynamic selectors that may potentially match the queried node.
|
||||
*/
|
||||
selectors: SelectorCore[];
|
||||
|
||||
/**
|
||||
* Gets a map of nodes to attributes and pseudo classes, that may affect the state of the dynamic
|
||||
*/
|
||||
changeMap: ChangeMap<T>;
|
||||
}
|
||||
|
||||
export function fromAstNodes(astRules: ParserNode[]): RuleSet[];
|
||||
|
||||
export function createSelector(sel: string): SelectorCore;
|
||||
587
nativescript-core/ui/styling/css-selector/css-selector.ts
Normal file
587
nativescript-core/ui/styling/css-selector/css-selector.ts
Normal file
@@ -0,0 +1,587 @@
|
||||
import { Node, Declaration, Changes, ChangeMap } from ".";
|
||||
import { isNullOrUndefined } from "../../../utils/types";
|
||||
import { escapeRegexSymbols } from "../../../utils/utils-common";
|
||||
|
||||
import * as cssParser from "../../../css";
|
||||
import * as parser from "../../../css/parser";
|
||||
|
||||
const enum Specificity {
|
||||
Inline = 0x01000000,
|
||||
Id = 0x00010000,
|
||||
Attribute = 0x00000100,
|
||||
Class = 0x00000100,
|
||||
PseudoClass = 0x00000100,
|
||||
Type = 0x00000001,
|
||||
Universal = 0x00000000,
|
||||
Invalid = 0x00000000
|
||||
}
|
||||
|
||||
const enum Rarity {
|
||||
Invalid = 4,
|
||||
Id = 3,
|
||||
Class = 2,
|
||||
Type = 1,
|
||||
PseudoClass = 0,
|
||||
Attribute = 0,
|
||||
Universal = 0,
|
||||
Inline = 0
|
||||
}
|
||||
|
||||
interface LookupSorter {
|
||||
sortById(id: string, sel: SelectorCore);
|
||||
sortByClass(cssClass: string, sel: SelectorCore);
|
||||
sortByType(cssType: string, sel: SelectorCore);
|
||||
sortAsUniversal(sel: SelectorCore);
|
||||
}
|
||||
|
||||
namespace Match {
|
||||
/**
|
||||
* Depends on attributes or pseudoclasses state;
|
||||
*/
|
||||
export const Dynamic = true;
|
||||
/**
|
||||
* Depends only on the tree structure.
|
||||
*/
|
||||
export const Static = false;
|
||||
}
|
||||
|
||||
function getNodeDirectSibling(node): null | Node {
|
||||
if (!node.parent || !node.parent.getChildIndex || !node.parent.getChildAt) {
|
||||
return null;
|
||||
}
|
||||
const nodeIndex = node.parent.getChildIndex(node);
|
||||
if (nodeIndex === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return node.parent.getChildAt(nodeIndex - 1);
|
||||
}
|
||||
|
||||
function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic: boolean = false): ClassDecorator {
|
||||
return cls => {
|
||||
cls.prototype.specificity = specificity;
|
||||
cls.prototype.rarity = rarity;
|
||||
cls.prototype.combinator = undefined;
|
||||
cls.prototype.dynamic = dynamic;
|
||||
|
||||
return cls;
|
||||
};
|
||||
}
|
||||
|
||||
declare type Combinator = "+" | ">" | "~" | " ";
|
||||
@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
|
||||
export abstract class SelectorCore {
|
||||
public specificity: number;
|
||||
public rarity: Rarity;
|
||||
public combinator: Combinator;
|
||||
public ruleset: RuleSet;
|
||||
public dynamic: boolean;
|
||||
public abstract match(node: Node): boolean;
|
||||
/**
|
||||
* If the selector is static returns if it matches the node.
|
||||
* If the selector is dynamic returns if it may match the node, and accumulates any changes that may affect its state.
|
||||
*/
|
||||
public abstract accumulateChanges(node: Node, map: ChangeAccumulator): boolean;
|
||||
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortAsUniversal(base || this); }
|
||||
}
|
||||
|
||||
export abstract class SimpleSelector extends SelectorCore {
|
||||
public accumulateChanges(node: Node, map?: ChangeAccumulator): boolean {
|
||||
if (!this.dynamic) {
|
||||
return this.match(node);
|
||||
} else if (this.mayMatch(node)) {
|
||||
this.trackChanges(node, map);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
public mayMatch(node: Node): boolean { return this.match(node); }
|
||||
public trackChanges(node: Node, map: ChangeAccumulator): void {
|
||||
// No-op, silence the tslint 'block is empty'.
|
||||
// Some derived classes (dynamic) will actually fill the map with stuff here, some (static) won't do anything.
|
||||
}
|
||||
}
|
||||
|
||||
function wrap(text: string): string {
|
||||
return text ? ` ${text} ` : "";
|
||||
}
|
||||
|
||||
@SelectorProperties(Specificity.Invalid, Rarity.Invalid, Match.Static)
|
||||
export class InvalidSelector extends SimpleSelector {
|
||||
constructor(public e: Error) { super(); }
|
||||
public toString(): string { return `<error: ${this.e}>`; }
|
||||
public match(node: Node): boolean { return false; }
|
||||
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
|
||||
// No-op, silence the tslint 'block is empty'.
|
||||
// It feels like tslint has problems with simple polymorphism...
|
||||
// This selector is invalid and will never match so we won't bother sorting it to further appear in queries.
|
||||
}
|
||||
}
|
||||
|
||||
@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
|
||||
export class UniversalSelector extends SimpleSelector {
|
||||
public toString(): string { return `*${wrap(this.combinator)}`; }
|
||||
public match(node: Node): boolean { return true; }
|
||||
}
|
||||
|
||||
@SelectorProperties(Specificity.Id, Rarity.Id, Match.Static)
|
||||
export class IdSelector extends SimpleSelector {
|
||||
constructor(public id: string) { super(); }
|
||||
public toString(): string { return `#${this.id}${wrap(this.combinator)}`; }
|
||||
public match(node: Node): boolean { return node.id === this.id; }
|
||||
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortById(this.id, base || this); }
|
||||
}
|
||||
|
||||
@SelectorProperties(Specificity.Type, Rarity.Type, Match.Static)
|
||||
export class TypeSelector extends SimpleSelector {
|
||||
constructor(public cssType: string) { super(); }
|
||||
public toString(): string { return `${this.cssType}${wrap(this.combinator)}`; }
|
||||
public match(node: Node): boolean { return node.cssType === this.cssType; }
|
||||
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortByType(this.cssType, base || this); }
|
||||
}
|
||||
|
||||
@SelectorProperties(Specificity.Class, Rarity.Class, Match.Static)
|
||||
export class ClassSelector extends SimpleSelector {
|
||||
constructor(public cssClass: string) { super(); }
|
||||
public toString(): string { return `.${this.cssClass}${wrap(this.combinator)}`; }
|
||||
public match(node: Node): boolean { return node.cssClasses && node.cssClasses.has(this.cssClass); }
|
||||
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortByClass(this.cssClass, base || this); }
|
||||
}
|
||||
|
||||
declare type AttributeTest = "=" | "^=" | "$=" | "*=" | "=" | "~=" | "|=";
|
||||
@SelectorProperties(Specificity.Attribute, Rarity.Attribute, Match.Dynamic)
|
||||
export class AttributeSelector extends SimpleSelector {
|
||||
constructor(public attribute: string, public test?: AttributeTest, public value?: string) {
|
||||
super();
|
||||
|
||||
if (!test) {
|
||||
// HasAttribute
|
||||
this.match = node => !isNullOrUndefined(node[attribute]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this.match = node => false;
|
||||
}
|
||||
|
||||
let escapedValue = escapeRegexSymbols(value);
|
||||
let regexp: RegExp = null;
|
||||
switch (test) {
|
||||
case "^=": // PrefixMatch
|
||||
regexp = new RegExp("^" + escapedValue);
|
||||
break;
|
||||
case "$=": // SuffixMatch
|
||||
regexp = new RegExp(escapedValue + "$");
|
||||
break;
|
||||
case "*=": // SubstringMatch
|
||||
regexp = new RegExp(escapedValue);
|
||||
break;
|
||||
case "=": // Equals
|
||||
regexp = new RegExp("^" + escapedValue + "$");
|
||||
break;
|
||||
case "~=": // Includes
|
||||
if (/\s/.test(value)) {
|
||||
this.match = node => false;
|
||||
|
||||
return;
|
||||
}
|
||||
regexp = new RegExp("(^|\\s)" + escapedValue + "(\\s|$)");
|
||||
break;
|
||||
case "|=": // DashMatch
|
||||
regexp = new RegExp("^" + escapedValue + "(-|$)");
|
||||
break;
|
||||
}
|
||||
|
||||
if (regexp) {
|
||||
this.match = node => regexp.test(node[attribute] + "");
|
||||
|
||||
return;
|
||||
} else {
|
||||
this.match = node => false;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
public toString(): string { return `[${this.attribute}${wrap(this.test)}${(this.test && this.value) || ""}]${wrap(this.combinator)}`; }
|
||||
public match(node: Node): boolean { return false; }
|
||||
public mayMatch(node: Node): boolean { return true; }
|
||||
public trackChanges(node: Node, map: ChangeAccumulator): void { map.addAttribute(node, this.attribute); }
|
||||
}
|
||||
|
||||
@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic)
|
||||
export class PseudoClassSelector extends SimpleSelector {
|
||||
constructor(public cssPseudoClass: string) { super(); }
|
||||
public toString(): string { return `:${this.cssPseudoClass}${wrap(this.combinator)}`; }
|
||||
public match(node: Node): boolean { return node.cssPseudoClasses && node.cssPseudoClasses.has(this.cssPseudoClass); }
|
||||
public mayMatch(node: Node): boolean { return true; }
|
||||
public trackChanges(node: Node, map: ChangeAccumulator): void { map.addPseudoClass(node, this.cssPseudoClass); }
|
||||
}
|
||||
|
||||
export class SimpleSelectorSequence extends SimpleSelector {
|
||||
private head: SimpleSelector;
|
||||
constructor(public selectors: SimpleSelector[]) {
|
||||
super();
|
||||
this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0);
|
||||
this.head = this.selectors.reduce((prev, curr) => !prev || (curr.rarity > prev.rarity) ? curr : prev, null);
|
||||
this.dynamic = selectors.some(sel => sel.dynamic);
|
||||
}
|
||||
public toString(): string { return `${this.selectors.join("")}${wrap(this.combinator)}`; }
|
||||
public match(node: Node): boolean { return this.selectors.every(sel => sel.match(node)); }
|
||||
public mayMatch(node: Node): boolean {
|
||||
return this.selectors.every(sel => sel.mayMatch(node));
|
||||
}
|
||||
public trackChanges(node, map): void {
|
||||
this.selectors.forEach(sel => sel.trackChanges(node, map));
|
||||
}
|
||||
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
|
||||
this.head.lookupSort(sorter, base || this);
|
||||
}
|
||||
}
|
||||
|
||||
export class Selector extends SelectorCore {
|
||||
// Grouped by ancestor combinators, then by direct child combinators.
|
||||
private groups: Selector.ChildGroup[];
|
||||
private last: SelectorCore;
|
||||
|
||||
constructor(public selectors: SimpleSelector[]) {
|
||||
super();
|
||||
const supportedCombinator = [undefined, " ", ">", "+"];
|
||||
let siblingGroup: SimpleSelector[];
|
||||
let lastGroup: SimpleSelector[][];
|
||||
let groups: SimpleSelector[][][] = [];
|
||||
selectors.reverse().forEach(sel => {
|
||||
if (supportedCombinator.indexOf(sel.combinator) === -1) {
|
||||
throw new Error(`Unsupported combinator "${sel.combinator}".`);
|
||||
}
|
||||
if (sel.combinator === undefined || sel.combinator === " ") {
|
||||
groups.push(lastGroup = [siblingGroup = []]);
|
||||
}
|
||||
if (sel.combinator === ">") {
|
||||
lastGroup.push(siblingGroup = []);
|
||||
}
|
||||
siblingGroup.push(sel);
|
||||
});
|
||||
this.groups = groups.map(g =>
|
||||
new Selector.ChildGroup(g.map(sg =>
|
||||
new Selector.SiblingGroup(sg)
|
||||
))
|
||||
);
|
||||
this.last = selectors[0];
|
||||
this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0);
|
||||
this.dynamic = selectors.some(sel => sel.dynamic);
|
||||
}
|
||||
|
||||
public toString(): string { return this.selectors.join(""); }
|
||||
|
||||
public match(node: Node): boolean {
|
||||
return this.groups.every((group, i) => {
|
||||
if (i === 0) {
|
||||
node = group.match(node);
|
||||
|
||||
return !!node;
|
||||
} else {
|
||||
let ancestor = node;
|
||||
while (ancestor = ancestor.parent) {
|
||||
if (node = group.match(ancestor)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
|
||||
this.last.lookupSort(sorter, this);
|
||||
}
|
||||
|
||||
public accumulateChanges(node: Node, map?: ChangeAccumulator): boolean {
|
||||
if (!this.dynamic) {
|
||||
return this.match(node);
|
||||
}
|
||||
|
||||
let bounds: Selector.Bound[] = [];
|
||||
let mayMatch = this.groups.every((group, i) => {
|
||||
if (i === 0) {
|
||||
let nextNode = group.mayMatch(node);
|
||||
bounds.push({ left: node, right: node });
|
||||
node = nextNode;
|
||||
|
||||
return !!node;
|
||||
} else {
|
||||
let ancestor = node;
|
||||
while (ancestor = ancestor.parent) {
|
||||
let nextNode = group.mayMatch(ancestor);
|
||||
if (nextNode) {
|
||||
bounds.push({ left: ancestor, right: null });
|
||||
node = nextNode;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Calculating the right bounds for each selectors won't save much
|
||||
if (!mayMatch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!map) {
|
||||
return mayMatch;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.groups.length; i++) {
|
||||
let group = this.groups[i];
|
||||
if (!group.dynamic) {
|
||||
continue;
|
||||
}
|
||||
let bound = bounds[i];
|
||||
let node = bound.left;
|
||||
do {
|
||||
if (group.mayMatch(node)) {
|
||||
group.trackChanges(node, map);
|
||||
}
|
||||
} while ((node !== bound.right) && (node = node.parent));
|
||||
}
|
||||
|
||||
return mayMatch;
|
||||
}
|
||||
}
|
||||
export namespace Selector {
|
||||
// Non-spec. Selector sequences are grouped by ancestor then by child combinators for easier backtracking.
|
||||
export class ChildGroup {
|
||||
public dynamic: boolean;
|
||||
|
||||
constructor(private selectors: SiblingGroup[]) {
|
||||
this.dynamic = selectors.some(sel => sel.dynamic);
|
||||
}
|
||||
|
||||
public match(node: Node): Node {
|
||||
return this.selectors.every((sel, i) => (i === 0 ? node : node = node.parent) && !!sel.match(node)) ? node : null;
|
||||
}
|
||||
|
||||
public mayMatch(node: Node): Node {
|
||||
return this.selectors.every((sel, i) => (i === 0 ? node : node = node.parent) && !!sel.mayMatch(node)) ? node : null;
|
||||
}
|
||||
|
||||
public trackChanges(node: Node, map: ChangeAccumulator) {
|
||||
this.selectors.forEach((sel, i) => (i === 0 ? node : node = node.parent) && sel.trackChanges(node, map));
|
||||
}
|
||||
}
|
||||
export class SiblingGroup {
|
||||
public dynamic: boolean;
|
||||
|
||||
constructor(private selectors: SimpleSelector[]) {
|
||||
this.dynamic = selectors.some(sel => sel.dynamic);
|
||||
}
|
||||
|
||||
public match(node: Node): Node {
|
||||
return this.selectors.every((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && sel.match(node)) ? node : null;
|
||||
}
|
||||
|
||||
public mayMatch(node: Node): Node {
|
||||
return this.selectors.every((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && sel.mayMatch(node)) ? node : null;
|
||||
}
|
||||
|
||||
public trackChanges(node: Node, map: ChangeAccumulator) {
|
||||
this.selectors.forEach((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && sel.trackChanges(node, map));
|
||||
}
|
||||
}
|
||||
export interface Bound {
|
||||
left: Node;
|
||||
right: Node;
|
||||
}
|
||||
}
|
||||
|
||||
export class RuleSet {
|
||||
constructor(public selectors: SelectorCore[], private declarations: Declaration[]) {
|
||||
this.selectors.forEach(sel => sel.ruleset = this);
|
||||
}
|
||||
public toString(): string { return `${this.selectors.join(", ")} {${this.declarations.map((d, i) => `${i === 0 ? " " : ""}${d.property}: ${d.value}`).join("; ")} }`; }
|
||||
public lookupSort(sorter: LookupSorter): void { this.selectors.forEach(sel => sel.lookupSort(sorter)); }
|
||||
}
|
||||
|
||||
export function fromAstNodes(astRules: cssParser.Node[]): RuleSet[] {
|
||||
return (<cssParser.Rule[]>astRules.filter(isRule)).map(rule => {
|
||||
let declarations = rule.declarations.filter(isDeclaration).map(createDeclaration);
|
||||
let selectors = rule.selectors.map(createSelector);
|
||||
let ruleset = new RuleSet(selectors, declarations);
|
||||
|
||||
return ruleset;
|
||||
});
|
||||
}
|
||||
|
||||
function createDeclaration(decl: cssParser.Declaration): any {
|
||||
return { property: decl.property.toLowerCase(), value: decl.value };
|
||||
}
|
||||
|
||||
function createSimpleSelectorFromAst(ast: parser.SimpleSelector): SimpleSelector {
|
||||
switch (ast.type) {
|
||||
case "*": return new UniversalSelector();
|
||||
case "#": return new IdSelector(ast.identifier);
|
||||
case "": return new TypeSelector(ast.identifier.replace(/-/, "").toLowerCase());
|
||||
case ".": return new ClassSelector(ast.identifier);
|
||||
case ":": return new PseudoClassSelector(ast.identifier);
|
||||
case "[]": return ast.test ? new AttributeSelector(ast.property, ast.test, ast.value) : new AttributeSelector(ast.property);
|
||||
}
|
||||
}
|
||||
|
||||
function createSimpleSelectorSequenceFromAst(ast: parser.SimpleSelectorSequence): SimpleSelectorSequence | SimpleSelector {
|
||||
if (ast.length === 0) {
|
||||
return new InvalidSelector(new Error("Empty simple selector sequence."));
|
||||
} else if (ast.length === 1) {
|
||||
return createSimpleSelectorFromAst(ast[0]);
|
||||
} else {
|
||||
return new SimpleSelectorSequence(ast.map(createSimpleSelectorFromAst));
|
||||
}
|
||||
}
|
||||
|
||||
function createSelectorFromAst(ast: parser.Selector): SimpleSelector | SimpleSelectorSequence | Selector {
|
||||
if (ast.length === 0) {
|
||||
return new InvalidSelector(new Error("Empty selector."));
|
||||
} else if (ast.length === 1) {
|
||||
return createSimpleSelectorSequenceFromAst(ast[0][0]);
|
||||
} else {
|
||||
let simpleSelectorSequences = [];
|
||||
let simpleSelectorSequence: SimpleSelectorSequence | SimpleSelector;
|
||||
let combinator: parser.Combinator;
|
||||
for (let i = 0; i < ast.length; i++) {
|
||||
simpleSelectorSequence = createSimpleSelectorSequenceFromAst(<parser.SimpleSelectorSequence>ast[i][0]);
|
||||
combinator = <parser.Combinator>ast[i][1];
|
||||
if (combinator) {
|
||||
simpleSelectorSequence.combinator = combinator;
|
||||
}
|
||||
simpleSelectorSequences.push(simpleSelectorSequence);
|
||||
}
|
||||
|
||||
return new Selector(simpleSelectorSequences);
|
||||
}
|
||||
}
|
||||
|
||||
export function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence | Selector {
|
||||
try {
|
||||
let parsedSelector = parser.parseSelector(sel);
|
||||
if (!parsedSelector) {
|
||||
return new InvalidSelector(new Error("Empty selector"));
|
||||
}
|
||||
|
||||
return createSelectorFromAst(parsedSelector.value);
|
||||
} catch (e) {
|
||||
return new InvalidSelector(e);
|
||||
}
|
||||
}
|
||||
|
||||
function isRule(node: cssParser.Node): node is cssParser.Rule {
|
||||
return node.type === "rule";
|
||||
}
|
||||
function isDeclaration(node: cssParser.Node): node is cssParser.Declaration {
|
||||
return node.type === "declaration";
|
||||
}
|
||||
|
||||
interface SelectorInDocument {
|
||||
pos: number;
|
||||
sel: SelectorCore;
|
||||
}
|
||||
interface SelectorMap {
|
||||
[key: string]: SelectorInDocument[];
|
||||
}
|
||||
export class SelectorsMap<T extends Node> implements LookupSorter {
|
||||
private id: SelectorMap = {};
|
||||
private class: SelectorMap = {};
|
||||
private type: SelectorMap = {};
|
||||
private universal: SelectorInDocument[] = [];
|
||||
|
||||
private position = 0;
|
||||
|
||||
constructor(rulesets: RuleSet[]) {
|
||||
rulesets.forEach(rule => rule.lookupSort(this));
|
||||
}
|
||||
|
||||
query(node: T): SelectorsMatch<T> {
|
||||
let selectorClasses = [
|
||||
this.universal,
|
||||
this.id[node.id],
|
||||
this.type[node.cssType]
|
||||
];
|
||||
if (node.cssClasses) {
|
||||
node.cssClasses.forEach(c => selectorClasses.push(this.class[c]));
|
||||
}
|
||||
let selectors = selectorClasses
|
||||
.filter(arr => !!arr)
|
||||
.reduce((cur, next) => cur.concat(next), []);
|
||||
|
||||
let selectorsMatch = new SelectorsMatch<T>();
|
||||
|
||||
selectorsMatch.selectors = selectors
|
||||
.filter(sel => sel.sel.accumulateChanges(node, selectorsMatch))
|
||||
.sort((a, b) => a.sel.specificity - b.sel.specificity || a.pos - b.pos)
|
||||
.map(docSel => docSel.sel);
|
||||
|
||||
return selectorsMatch;
|
||||
}
|
||||
|
||||
sortById(id: string, sel: SelectorCore): void { this.addToMap(this.id, id, sel); }
|
||||
sortByClass(cssClass: string, sel: SelectorCore): void {
|
||||
this.addToMap(this.class, cssClass, sel);
|
||||
}
|
||||
sortByType(cssType: string, sel: SelectorCore): void {
|
||||
this.addToMap(this.type, cssType, sel);
|
||||
}
|
||||
sortAsUniversal(sel: SelectorCore): void { this.universal.push(this.makeDocSelector(sel)); }
|
||||
|
||||
private addToMap(map: SelectorMap, head: string, sel: SelectorCore): void {
|
||||
this.position++;
|
||||
let list = map[head];
|
||||
if (list) {
|
||||
list.push(this.makeDocSelector(sel));
|
||||
} else {
|
||||
map[head] = [this.makeDocSelector(sel)];
|
||||
}
|
||||
}
|
||||
|
||||
private makeDocSelector(sel: SelectorCore): SelectorInDocument {
|
||||
return { sel, pos: this.position++ };
|
||||
}
|
||||
}
|
||||
|
||||
interface ChangeAccumulator {
|
||||
addAttribute(node: Node, attribute: string): void;
|
||||
addPseudoClass(node: Node, pseudoClass: string): void;
|
||||
}
|
||||
|
||||
export class SelectorsMatch<T extends Node> implements ChangeAccumulator {
|
||||
public changeMap: ChangeMap<T> = new Map<T, Changes>();
|
||||
public selectors;
|
||||
|
||||
public addAttribute(node: T, attribute: string): void {
|
||||
let deps: Changes = this.properties(node);
|
||||
if (!deps.attributes) {
|
||||
deps.attributes = new Set();
|
||||
}
|
||||
deps.attributes.add(attribute);
|
||||
}
|
||||
|
||||
public addPseudoClass(node: T, pseudoClass: string): void {
|
||||
let deps: Changes = this.properties(node);
|
||||
if (!deps.pseudoClasses) {
|
||||
deps.pseudoClasses = new Set();
|
||||
}
|
||||
deps.pseudoClasses.add(pseudoClass);
|
||||
}
|
||||
|
||||
private properties(node: T): Changes {
|
||||
let set = this.changeMap.get(node);
|
||||
if (!set) {
|
||||
this.changeMap.set(node, set = {});
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
}
|
||||
5
nativescript-core/ui/styling/css-selector/package.json
Normal file
5
nativescript-core/ui/styling/css-selector/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "css-selector",
|
||||
"main": "css-selector",
|
||||
"types": "css-selector.d.ts"
|
||||
}
|
||||
172
nativescript-core/ui/styling/font-common.ts
Normal file
172
nativescript-core/ui/styling/font-common.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { Font as FontDefinition } from "./font";
|
||||
import { ParsedFont } from "./font-interfaces";
|
||||
import { makeValidator, makeParser } from "../core/properties";
|
||||
|
||||
export * from "./font-interfaces";
|
||||
|
||||
export abstract class Font implements FontDefinition {
|
||||
public static default = undefined;
|
||||
|
||||
get isItalic(): boolean {
|
||||
return this.fontStyle === FontStyle.ITALIC;
|
||||
}
|
||||
|
||||
get isBold(): boolean {
|
||||
return this.fontWeight === FontWeight.SEMI_BOLD ||
|
||||
this.fontWeight === FontWeight.BOLD ||
|
||||
this.fontWeight === "700" ||
|
||||
this.fontWeight === FontWeight.EXTRA_BOLD ||
|
||||
this.fontWeight === FontWeight.BLACK;
|
||||
}
|
||||
|
||||
protected constructor(
|
||||
public readonly fontFamily: string,
|
||||
public readonly fontSize: number,
|
||||
public readonly fontStyle: FontStyle,
|
||||
public readonly fontWeight: FontWeight) {
|
||||
}
|
||||
|
||||
public abstract getAndroidTypeface(): any /* android.graphics.Typeface */;
|
||||
public abstract getUIFont(defaultFont: any /* UIFont */): any /* UIFont */;
|
||||
public abstract withFontFamily(family: string): Font;
|
||||
public abstract withFontStyle(style: string): Font;
|
||||
public abstract withFontWeight(weight: string): Font;
|
||||
public abstract withFontSize(size: number): Font;
|
||||
|
||||
public static equals(value1: Font, value2: Font): boolean {
|
||||
// both values are falsy
|
||||
if (!value1 && !value2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// only one is falsy
|
||||
if (!value1 || !value2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value1.fontFamily === value2.fontFamily &&
|
||||
value1.fontSize === value2.fontSize &&
|
||||
value1.fontStyle === value2.fontStyle &&
|
||||
value1.fontWeight === value2.fontWeight;
|
||||
}
|
||||
}
|
||||
|
||||
export type FontStyle = "normal" | "italic";
|
||||
export namespace FontStyle {
|
||||
export const NORMAL: "normal" = "normal";
|
||||
export const ITALIC: "italic" = "italic";
|
||||
export const isValid = makeValidator<FontStyle>(NORMAL, ITALIC);
|
||||
export const parse = makeParser<FontStyle>(isValid);
|
||||
}
|
||||
|
||||
export type FontWeight = "100" | "200" | "300" | "normal" | "400" | "500" | "600" | "bold" | "700" | "800" | "900";
|
||||
export namespace FontWeight {
|
||||
export const THIN: "100" = "100";
|
||||
export const EXTRA_LIGHT: "200" = "200";
|
||||
export const LIGHT: "300" = "300";
|
||||
export const NORMAL: "normal" = "normal";
|
||||
export const MEDIUM: "500" = "500";
|
||||
export const SEMI_BOLD: "600" = "600";
|
||||
export const BOLD: "bold" = "bold";
|
||||
export const EXTRA_BOLD: "800" = "800";
|
||||
export const BLACK: "900" = "900";
|
||||
export const isValid = makeValidator<FontWeight>(THIN, EXTRA_LIGHT, LIGHT, NORMAL, "400", MEDIUM, SEMI_BOLD, BOLD, "700", EXTRA_BOLD, BLACK);
|
||||
export const parse = makeParser<FontStyle>(isValid);
|
||||
}
|
||||
|
||||
export function parseFontFamily(value: string): Array<string> {
|
||||
const result = new Array<string>();
|
||||
if (!value) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const split = value.split(",");
|
||||
for (let i = 0; i < split.length; i++) {
|
||||
let str = split[i].trim().replace(/['"]+/g, "");
|
||||
if (str) {
|
||||
result.push(str);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export module genericFontFamilies {
|
||||
export const serif = "serif";
|
||||
export const sansSerif = "sans-serif";
|
||||
export const monospace = "monospace";
|
||||
export const system = "system";
|
||||
}
|
||||
|
||||
const styles = new Set();
|
||||
[
|
||||
FontStyle.NORMAL,
|
||||
FontStyle.ITALIC
|
||||
].forEach((val, i, a) => styles.add(val));
|
||||
|
||||
// http://www.w3schools.com/cssref/pr_font_weight.asp
|
||||
//- normal(same as 400)
|
||||
//- bold(same as 700)
|
||||
//- 100(Thin) (API16 -thin)
|
||||
//- 200(Extra Light / Ultra Light) (API16 -light)
|
||||
//- 300(Light) (API16 -light)
|
||||
//- 400(Normal)
|
||||
//- 500(Medium) (API21 -medium)
|
||||
//- 600(Semi Bold / Demi Bold) (API21 -medium)
|
||||
//- 700(Bold) (API16 -bold)
|
||||
//- 800(Extra Bold / Ultra Bold) (API16 -bold)
|
||||
//- 900(Black / Heavy) (API21 -black)
|
||||
const weights = new Set();
|
||||
[
|
||||
FontWeight.THIN,
|
||||
FontWeight.EXTRA_LIGHT,
|
||||
FontWeight.LIGHT,
|
||||
FontWeight.NORMAL,
|
||||
"400",
|
||||
FontWeight.MEDIUM,
|
||||
FontWeight.SEMI_BOLD,
|
||||
FontWeight.BOLD,
|
||||
"700",
|
||||
FontWeight.EXTRA_BOLD,
|
||||
FontWeight.BLACK
|
||||
].forEach((val, i, a) => weights.add(val));
|
||||
|
||||
export function parseFont(fontValue: string): ParsedFont {
|
||||
let result: ParsedFont = {
|
||||
fontStyle: "normal",
|
||||
fontVariant: "normal",
|
||||
fontWeight: "normal"
|
||||
};
|
||||
|
||||
const parts = fontValue.split(/\s+/);
|
||||
let part: string;
|
||||
while (part = parts.shift()) {
|
||||
if (part === "normal") {
|
||||
// nothing to do here
|
||||
}
|
||||
else if (part === "small-caps") {
|
||||
// The only supported font variant in shorthand font
|
||||
result.fontVariant = part;
|
||||
}
|
||||
else if (styles.has(part)) {
|
||||
result.fontStyle = <any>part;
|
||||
}
|
||||
else if (weights.has(part)) {
|
||||
result.fontWeight = <any>part;
|
||||
}
|
||||
else if (!result.fontSize) {
|
||||
let sizes = part.split("/");
|
||||
result.fontSize = sizes[0];
|
||||
result.lineHeight = sizes.length > 1 ? sizes[1] : undefined;
|
||||
}
|
||||
else {
|
||||
result.fontFamily = part;
|
||||
if (parts.length) {
|
||||
result.fontFamily += " " + parts.join(" ");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
12
nativescript-core/ui/styling/font-interfaces.ts
Normal file
12
nativescript-core/ui/styling/font-interfaces.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type FontStyle = "normal" | "italic";
|
||||
|
||||
export type FontWeight = "100" | "200" | "300" | "normal" | "400" | "500" | "600" | "bold" | "700" | "800" | "900";
|
||||
|
||||
export interface ParsedFont {
|
||||
fontStyle?: FontStyle;
|
||||
fontVariant?: string;
|
||||
fontWeight?: FontWeight;
|
||||
lineHeight?: string;
|
||||
fontSize?: string;
|
||||
fontFamily?: string;
|
||||
}
|
||||
163
nativescript-core/ui/styling/font.android.ts
Normal file
163
nativescript-core/ui/styling/font.android.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Font as FontBase, parseFontFamily, genericFontFamilies, FontWeight } from "./font-common";
|
||||
import { isEnabled as traceEnabled, write as traceWrite, categories as traceCategories, messageType as traceMessageType } from "../../trace";
|
||||
import * as application from "../../application";
|
||||
import * as fs from "../../file-system";
|
||||
|
||||
export * from "./font-common";
|
||||
|
||||
const FONTS_BASE_PATH = "/fonts/";
|
||||
const typefaceCache = new Map<string, android.graphics.Typeface>();
|
||||
let appAssets: android.content.res.AssetManager;
|
||||
|
||||
export class Font extends FontBase {
|
||||
public static default = new Font(undefined, undefined, "normal", "normal");
|
||||
|
||||
private _typeface: android.graphics.Typeface;
|
||||
|
||||
constructor(family: string, size: number, style: "normal" | "italic", weight: FontWeight) {
|
||||
super(family, size, style, weight);
|
||||
}
|
||||
|
||||
public withFontFamily(family: string): Font {
|
||||
return new Font(family, this.fontSize, this.fontStyle, this.fontWeight);
|
||||
}
|
||||
|
||||
public withFontStyle(style: "normal" | "italic"): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, style, this.fontWeight);
|
||||
}
|
||||
|
||||
public withFontWeight(weight: FontWeight): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, this.fontStyle, weight);
|
||||
}
|
||||
|
||||
public withFontSize(size: number): Font {
|
||||
return new Font(this.fontFamily, size, this.fontStyle, this.fontWeight);
|
||||
}
|
||||
|
||||
public getAndroidTypeface(): android.graphics.Typeface {
|
||||
if (!this._typeface) {
|
||||
this._typeface = createTypeface(this);
|
||||
}
|
||||
|
||||
return this._typeface;
|
||||
}
|
||||
|
||||
public getUIFont(defaultFont: UIFont): UIFont {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function loadFontFromFile(fontFamily: string): android.graphics.Typeface {
|
||||
appAssets = appAssets || application.android.context.getAssets();
|
||||
if (!appAssets) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let result = typefaceCache.get(fontFamily);
|
||||
// Check for undefined explicitly as null mean we tried to load the font, but failed.
|
||||
if (result === undefined) {
|
||||
result = null;
|
||||
|
||||
let fontAssetPath: string;
|
||||
const basePath = fs.path.join(fs.knownFolders.currentApp().path, "fonts", fontFamily);
|
||||
if (fs.File.exists(basePath + ".ttf")) {
|
||||
fontAssetPath = FONTS_BASE_PATH + fontFamily + ".ttf";
|
||||
}
|
||||
else if (fs.File.exists(basePath + ".otf")) {
|
||||
fontAssetPath = FONTS_BASE_PATH + fontFamily + ".otf";
|
||||
}
|
||||
else {
|
||||
if (traceEnabled()) {
|
||||
traceWrite("Could not find font file for " + fontFamily, traceCategories.Error, traceMessageType.error);
|
||||
}
|
||||
}
|
||||
|
||||
if (fontAssetPath) {
|
||||
try {
|
||||
fontAssetPath = fs.path.join(fs.knownFolders.currentApp().path, fontAssetPath);
|
||||
result = android.graphics.Typeface.createFromFile(fontAssetPath);
|
||||
} catch (e) {
|
||||
if (traceEnabled()) {
|
||||
traceWrite("Error loading font asset: " + fontAssetPath, traceCategories.Error, traceMessageType.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
typefaceCache.set(fontFamily, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function createTypeface(font: Font): android.graphics.Typeface {
|
||||
let fontStyle = 0;
|
||||
if (font.isBold) {
|
||||
fontStyle |= android.graphics.Typeface.BOLD;
|
||||
}
|
||||
if (font.isItalic) {
|
||||
fontStyle |= android.graphics.Typeface.ITALIC;
|
||||
}
|
||||
|
||||
//http://stackoverflow.com/questions/19691530/valid-values-for-androidfontfamily-and-what-they-map-to
|
||||
const fonts = parseFontFamily(font.fontFamily);
|
||||
let result = null;
|
||||
for (let i = 0; i < fonts.length; i++) {
|
||||
switch (fonts[i].toLowerCase()) {
|
||||
case genericFontFamilies.serif:
|
||||
result = android.graphics.Typeface.create("serif" + getFontWeightSuffix(font.fontWeight), fontStyle);
|
||||
break;
|
||||
|
||||
case genericFontFamilies.sansSerif:
|
||||
case genericFontFamilies.system:
|
||||
result = android.graphics.Typeface.create("sans-serif" + getFontWeightSuffix(font.fontWeight), fontStyle);
|
||||
break;
|
||||
|
||||
case genericFontFamilies.monospace:
|
||||
result = android.graphics.Typeface.create("monospace" + getFontWeightSuffix(font.fontWeight), fontStyle);
|
||||
break;
|
||||
|
||||
default:
|
||||
result = loadFontFromFile(fonts[i]);
|
||||
if (result && fontStyle) {
|
||||
result = android.graphics.Typeface.create(result, fontStyle);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
// Found the font!
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
result = android.graphics.Typeface.create("sans-serif" + getFontWeightSuffix(font.fontWeight), fontStyle);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getFontWeightSuffix(fontWeight: FontWeight): string {
|
||||
switch (fontWeight) {
|
||||
case FontWeight.THIN:
|
||||
return android.os.Build.VERSION.SDK_INT >= 16 ? "-thin" : "";
|
||||
case FontWeight.EXTRA_LIGHT:
|
||||
case FontWeight.LIGHT:
|
||||
return android.os.Build.VERSION.SDK_INT >= 16 ? "-light" : "";
|
||||
case FontWeight.NORMAL:
|
||||
case "400":
|
||||
case undefined:
|
||||
case null:
|
||||
return "";
|
||||
case FontWeight.MEDIUM:
|
||||
case FontWeight.SEMI_BOLD:
|
||||
return android.os.Build.VERSION.SDK_INT >= 21 ? "-medium" : "";
|
||||
case FontWeight.BOLD:
|
||||
case "700":
|
||||
case FontWeight.EXTRA_BOLD:
|
||||
return "";
|
||||
case FontWeight.BLACK:
|
||||
return android.os.Build.VERSION.SDK_INT >= 21 ? "-black" : "";
|
||||
default:
|
||||
throw new Error(`Invalid font weight: "${fontWeight}"`);
|
||||
}
|
||||
}
|
||||
65
nativescript-core/ui/styling/font.d.ts
vendored
Normal file
65
nativescript-core/ui/styling/font.d.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @module "ui/styling/font"
|
||||
*/ /** */
|
||||
|
||||
export class Font {
|
||||
public static default: Font;
|
||||
|
||||
public fontFamily: string;
|
||||
public fontStyle: FontStyle;
|
||||
public fontWeight: FontWeight;
|
||||
public fontSize: number;
|
||||
|
||||
public isBold: boolean;
|
||||
public isItalic: boolean;
|
||||
|
||||
constructor(family: string, size: number, style: FontStyle, weight: FontWeight);
|
||||
|
||||
public getAndroidTypeface(): any /* android.graphics.Typeface */;
|
||||
public getUIFont(defaultFont: any /* UIFont */): any /* UIFont */;
|
||||
|
||||
public withFontFamily(family: string): Font;
|
||||
public withFontStyle(style: string): Font;
|
||||
public withFontWeight(weight: string): Font;
|
||||
public withFontSize(size: number): Font;
|
||||
|
||||
public static equals(value1: Font, value2: Font): boolean;
|
||||
}
|
||||
|
||||
export type FontStyle = "normal" | "italic";
|
||||
export namespace FontStyle {
|
||||
export const NORMAL: "normal";
|
||||
export const ITALIC: "italic";
|
||||
export function isValid(value: any): boolean;
|
||||
export function parse(value: string): FontStyle;
|
||||
}
|
||||
|
||||
export type FontWeight = "100" | "200" | "300" | "normal" | "400" | "500" | "600" | "bold" | "700" | "800" | "900";
|
||||
export namespace FontWeight {
|
||||
export const THIN: "100";
|
||||
export const EXTRA_LIGHT: "200";
|
||||
export const LIGHT: "300";
|
||||
export const NORMAL: "normal";
|
||||
export const MEDIUM: "500";
|
||||
export const SEMI_BOLD: "600";
|
||||
export const BOLD: "bold";
|
||||
export const EXTRA_BOLD: "800";
|
||||
export const BLACK: "900";
|
||||
export function isValid(value: any): boolean;
|
||||
export function parse(value: string): FontWeight;
|
||||
}
|
||||
|
||||
export interface ParsedFont {
|
||||
fontStyle?: FontStyle;
|
||||
fontVariant?: string;
|
||||
fontWeight?: FontWeight,
|
||||
lineHeight?: string,
|
||||
fontSize?: string,
|
||||
fontFamily?: string
|
||||
}
|
||||
|
||||
export function parseFont(fontValue: string): ParsedFont;
|
||||
|
||||
export module ios {
|
||||
export function registerFont(fontFile: string);
|
||||
}
|
||||
226
nativescript-core/ui/styling/font.ios.ts
Normal file
226
nativescript-core/ui/styling/font.ios.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { Font as FontBase, parseFontFamily, genericFontFamilies, FontStyle, FontWeight } from "./font-common";
|
||||
import { isEnabled as traceEnabled, write as traceWrite, categories as traceCategories, messageType as traceMessageType } from "../../trace";
|
||||
import { device } from "../../platform";
|
||||
import * as fs from "../../file-system";
|
||||
export * from "./font-common";
|
||||
|
||||
const EMULATE_OBLIQUE = true;
|
||||
const OBLIQUE_TRANSFORM = CGAffineTransformMake(1, 0, 0.2, 1, 0, 0);
|
||||
const DEFAULT_SERIF = "Times New Roman";
|
||||
const DEFAULT_MONOSPACE = "Courier New";
|
||||
const SUPPORT_FONT_WEIGHTS = parseFloat(device.osVersion) >= 10.0;
|
||||
|
||||
export class Font extends FontBase {
|
||||
public static default = new Font(undefined, undefined, FontStyle.NORMAL, FontWeight.NORMAL);
|
||||
|
||||
private _uiFont: UIFont;
|
||||
|
||||
constructor(family: string, size: number, style: FontStyle, weight: FontWeight) {
|
||||
super(family, size, style, weight);
|
||||
}
|
||||
|
||||
public withFontFamily(family: string): Font {
|
||||
return new Font(family, this.fontSize, this.fontStyle, this.fontWeight);
|
||||
}
|
||||
|
||||
public withFontStyle(style: FontStyle): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, style, this.fontWeight);
|
||||
}
|
||||
|
||||
public withFontWeight(weight: FontWeight): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, this.fontStyle, weight);
|
||||
}
|
||||
|
||||
public withFontSize(size: number): Font {
|
||||
return new Font(this.fontFamily, size, this.fontStyle, this.fontWeight);
|
||||
}
|
||||
|
||||
public getUIFont(defaultFont: UIFont): UIFont {
|
||||
if (!this._uiFont) {
|
||||
this._uiFont = createUIFont(this, defaultFont);
|
||||
}
|
||||
|
||||
return this._uiFont;
|
||||
}
|
||||
|
||||
public getAndroidTypeface(): android.graphics.Typeface {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getFontFamilyRespectingGenericFonts(fontFamily: string): string {
|
||||
if (!fontFamily) {
|
||||
return fontFamily;
|
||||
}
|
||||
|
||||
switch (fontFamily.toLowerCase()) {
|
||||
case genericFontFamilies.serif:
|
||||
return DEFAULT_SERIF;
|
||||
|
||||
case genericFontFamilies.monospace:
|
||||
return DEFAULT_MONOSPACE;
|
||||
|
||||
default:
|
||||
return fontFamily;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldUseSystemFont(fontFamily: string) {
|
||||
return !fontFamily ||
|
||||
fontFamily === genericFontFamilies.sansSerif ||
|
||||
fontFamily === genericFontFamilies.system;
|
||||
}
|
||||
|
||||
function getNativeFontWeight(fontWeight: FontWeight): number {
|
||||
switch (fontWeight) {
|
||||
case FontWeight.THIN:
|
||||
return UIFontWeightUltraLight;
|
||||
case FontWeight.EXTRA_LIGHT:
|
||||
return UIFontWeightThin;
|
||||
case FontWeight.LIGHT:
|
||||
return UIFontWeightLight;
|
||||
case FontWeight.NORMAL:
|
||||
case "400":
|
||||
case undefined:
|
||||
case null:
|
||||
return UIFontWeightRegular;
|
||||
case FontWeight.MEDIUM:
|
||||
return UIFontWeightMedium;
|
||||
case FontWeight.SEMI_BOLD:
|
||||
return UIFontWeightSemibold;
|
||||
case FontWeight.BOLD:
|
||||
case "700":
|
||||
return UIFontWeightBold;
|
||||
case FontWeight.EXTRA_BOLD:
|
||||
return UIFontWeightHeavy;
|
||||
case FontWeight.BLACK:
|
||||
return UIFontWeightBlack;
|
||||
default:
|
||||
throw new Error(`Invalid font weight: "${fontWeight}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemFont(size: number, nativeWeight: number, italic: boolean, symbolicTraits: number): UIFont {
|
||||
let result = UIFont.systemFontOfSizeWeight(size, nativeWeight);
|
||||
if (italic) {
|
||||
let descriptor = result.fontDescriptor.fontDescriptorWithSymbolicTraits(symbolicTraits);
|
||||
result = UIFont.fontWithDescriptorSize(descriptor, size);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function createUIFont(font: Font, defaultFont: UIFont): UIFont {
|
||||
let result: UIFont;
|
||||
const size = font.fontSize || defaultFont.pointSize;
|
||||
const nativeWeight = getNativeFontWeight(font.fontWeight);
|
||||
const fontFamilies = parseFontFamily(font.fontFamily);
|
||||
|
||||
let symbolicTraits: number = 0;
|
||||
if (font.isBold) {
|
||||
symbolicTraits |= UIFontDescriptorSymbolicTraits.TraitBold;
|
||||
}
|
||||
if (font.isItalic) {
|
||||
symbolicTraits |= UIFontDescriptorSymbolicTraits.TraitItalic;
|
||||
}
|
||||
|
||||
let fontDescriptorTraits = {
|
||||
[UIFontSymbolicTrait]: symbolicTraits
|
||||
};
|
||||
|
||||
// IOS versions below 10 will not return the correct font when UIFontWeightTrait is set
|
||||
// In this case - rely on UIFontSymbolicTrait only
|
||||
if (SUPPORT_FONT_WEIGHTS) {
|
||||
fontDescriptorTraits[UIFontWeightTrait] = nativeWeight;
|
||||
}
|
||||
|
||||
for (let i = 0; i < fontFamilies.length; i++) {
|
||||
const fontFamily = getFontFamilyRespectingGenericFonts(fontFamilies[i]);
|
||||
|
||||
if (shouldUseSystemFont(fontFamily)) {
|
||||
result = getSystemFont(size, nativeWeight, font.isItalic, symbolicTraits);
|
||||
break;
|
||||
} else {
|
||||
const fontAttributes = {
|
||||
[UIFontDescriptorFamilyAttribute]: fontFamily,
|
||||
[UIFontDescriptorTraitsAttribute]: fontDescriptorTraits
|
||||
};
|
||||
|
||||
let descriptor = UIFontDescriptor.fontDescriptorWithFontAttributes(<any>fontAttributes);
|
||||
result = UIFont.fontWithDescriptorSize(descriptor, size);
|
||||
|
||||
let actualItalic = result.fontDescriptor.symbolicTraits & UIFontDescriptorSymbolicTraits.TraitItalic;
|
||||
if (font.isItalic && !actualItalic && EMULATE_OBLIQUE) {
|
||||
// The font we got is not actually italic so emulate that with a matrix
|
||||
descriptor = descriptor.fontDescriptorWithMatrix(OBLIQUE_TRANSFORM);
|
||||
result = UIFont.fontWithDescriptorSize(descriptor, size);
|
||||
}
|
||||
|
||||
// Check if the resolved font has the correct font-family
|
||||
// If not - fallback to the next font-family
|
||||
if (result.familyName === fontFamily) {
|
||||
break;
|
||||
} else {
|
||||
result = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't resolve font - fallback to the system font
|
||||
if (!result) {
|
||||
result = getSystemFont(size, nativeWeight, font.isItalic, symbolicTraits);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export module ios {
|
||||
export function registerFont(fontFile: string) {
|
||||
let filePath = fs.path.join(fs.knownFolders.currentApp().path, "fonts", fontFile);
|
||||
if (!fs.File.exists(filePath)) {
|
||||
filePath = fs.path.join(fs.knownFolders.currentApp().path, fontFile);
|
||||
}
|
||||
const fontData = NSFileManager.defaultManager.contentsAtPath(filePath);
|
||||
if (!fontData) {
|
||||
throw new Error("Could not load font from: " + fontFile);
|
||||
}
|
||||
const provider = CGDataProviderCreateWithCFData(fontData);
|
||||
const font = CGFontCreateWithDataProvider(provider);
|
||||
|
||||
if (!font) {
|
||||
throw new Error("Could not load font from: " + fontFile);
|
||||
}
|
||||
|
||||
const error = new interop.Reference<NSError>();
|
||||
if (!CTFontManagerRegisterGraphicsFont(font, error)) {
|
||||
if (traceEnabled()) {
|
||||
traceWrite("Error occur while registering font: " + CFErrorCopyDescription(<NSError>error.value), traceCategories.Error, traceMessageType.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerFontsInFolder(fontsFolderPath) {
|
||||
const fontsFolder = fs.Folder.fromPath(fontsFolderPath);
|
||||
|
||||
fontsFolder.eachEntity((fileEntity: fs.FileSystemEntity) => {
|
||||
if (fs.Folder.exists(fs.path.join(fontsFolderPath, fileEntity.name))) {
|
||||
return true;
|
||||
}
|
||||
if (fileEntity instanceof fs.File &&
|
||||
((<fs.File>fileEntity).extension === ".ttf" || (<fs.File>fileEntity).extension === ".otf")) {
|
||||
ios.registerFont(fileEntity.name);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function registerCustomFonts() {
|
||||
const appDir = fs.knownFolders.currentApp().path;
|
||||
const fontsDir = fs.path.join(appDir, "fonts");
|
||||
if (fs.Folder.exists(fontsDir)) {
|
||||
registerFontsInFolder(fontsDir);
|
||||
}
|
||||
}
|
||||
registerCustomFonts();
|
||||
21
nativescript-core/ui/styling/gradient.d.ts
vendored
Normal file
21
nativescript-core/ui/styling/gradient.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @module "ui/styling/gradient"
|
||||
*/ /** */
|
||||
|
||||
import { LengthPercentUnit } from "./style-properties";
|
||||
import { Color } from "../../color";
|
||||
import { LinearGradient as LinearGradientDefinition } from "../../css/parser";
|
||||
|
||||
export class LinearGradient {
|
||||
public angle: number;
|
||||
public colorStops: ColorStop[];
|
||||
|
||||
public static parse(value: LinearGradientDefinition): LinearGradientDefinition;
|
||||
|
||||
public static equals(first: LinearGradientDefinition, second: LinearGradientDefinition): boolean;
|
||||
}
|
||||
|
||||
export interface ColorStop {
|
||||
color: Color;
|
||||
offset?: LengthPercentUnit;
|
||||
}
|
||||
61
nativescript-core/ui/styling/linear-gradient.ts
Normal file
61
nativescript-core/ui/styling/linear-gradient.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { LengthPercentUnit } from "./style-properties";
|
||||
import { Color } from "../../color";
|
||||
import { ColorStop } from "./gradient";
|
||||
import * as parser from "../../css/parser";
|
||||
|
||||
export class LinearGradient {
|
||||
public angle: number;
|
||||
public colorStops: ColorStop[];
|
||||
|
||||
public static parse(value: parser.LinearGradient): LinearGradient {
|
||||
const result = new LinearGradient();
|
||||
result.angle = value.angle;
|
||||
result.colorStops = value.colors.map(color => {
|
||||
const offset = color.offset || null;
|
||||
let offsetUnit: LengthPercentUnit;
|
||||
|
||||
if (offset && offset.unit === "%") {
|
||||
offsetUnit = {
|
||||
unit: "%",
|
||||
value: offset.value
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
color: new Color(color.argb),
|
||||
offset: offsetUnit
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static equals(first: LinearGradient, second: LinearGradient): boolean {
|
||||
if (!first && !second) {
|
||||
return true;
|
||||
} else if (!first || !second) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (first.angle !== second.angle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (first.colorStops.length !== second.colorStops.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < first.colorStops.length; i++) {
|
||||
const firstStop = first.colorStops[i];
|
||||
const secondStop = second.colorStops[i];
|
||||
if (firstStop.offset !== secondStop.offset) {
|
||||
return false;
|
||||
}
|
||||
if (!Color.equals(firstStop.color, secondStop.color)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
117
nativescript-core/ui/styling/style-properties.d.ts
vendored
Normal file
117
nativescript-core/ui/styling/style-properties.d.ts
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @module "ui/styling/style-properties"
|
||||
*/ /** */
|
||||
|
||||
import { TransformFunctionsInfo } from "../animation/animation";
|
||||
import { Color } from "../../color";
|
||||
import { Style, CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from "../core/properties";
|
||||
import { Font, FontStyle, FontWeight } from "./font";
|
||||
import { Background } from "./background";
|
||||
import { dip, px, percent } from "../core/view";
|
||||
|
||||
export type LengthDipUnit = { readonly unit: "dip", readonly value: dip };
|
||||
export type LengthPxUnit = { readonly unit: "px", readonly value: px };
|
||||
export type LengthPercentUnit = { readonly unit: "%", readonly value: percent };
|
||||
|
||||
export type Length = "auto" | dip | LengthDipUnit | LengthPxUnit;
|
||||
export type PercentLength = "auto" | dip | LengthDipUnit | LengthPxUnit | LengthPercentUnit;
|
||||
|
||||
export namespace Length {
|
||||
export function parse(text: string): Length;
|
||||
export function equals(a: Length, b: Length): boolean;
|
||||
/**
|
||||
* Converts Length unit to device pixels.
|
||||
* @param length The Length to convert.
|
||||
* @param auto Value to use for conversion of "auto". By default is Math.NaN.
|
||||
*/
|
||||
export function toDevicePixels(length: Length, auto?: number): number;
|
||||
export function convertToString(length: Length): string;
|
||||
}
|
||||
|
||||
export namespace PercentLength {
|
||||
export function parse(text: string): PercentLength;
|
||||
export function equals(a: PercentLength, b: PercentLength): boolean;
|
||||
/**
|
||||
* Converts PercentLength unit to device pixels.
|
||||
* @param length The PercentLength to convert.
|
||||
* @param auto Value to use for conversion of "auto". By default is Math.NaN.
|
||||
* @param parentAvailableWidth Value to use as base when converting percent unit. By default is Math.NaN.
|
||||
*/
|
||||
export function toDevicePixels(length: PercentLength, auto?: number, parentAvailableWidth?: px): number;
|
||||
export function convertToString(length: PercentLength): string;
|
||||
}
|
||||
|
||||
export const zeroLength: Length;
|
||||
|
||||
export const rotateProperty: CssAnimationProperty<Style, number>;
|
||||
export const scaleXProperty: CssAnimationProperty<Style, number>;
|
||||
export const scaleYProperty: CssAnimationProperty<Style, number>;
|
||||
export const translateXProperty: CssAnimationProperty<Style, dip>;
|
||||
export const translateYProperty: CssAnimationProperty<Style, dip>;
|
||||
|
||||
export function transformConverter(text: string): TransformFunctionsInfo;
|
||||
|
||||
export const clipPathProperty: CssProperty<Style, string>;
|
||||
export const colorProperty: InheritedCssProperty<Style, Color>;
|
||||
|
||||
export const backgroundProperty: ShorthandProperty<Style, string>;
|
||||
export const backgroundColorProperty: CssAnimationProperty<Style, Color>;
|
||||
export const backgroundImageProperty: CssProperty<Style, string>;
|
||||
export const backgroundRepeatProperty: CssProperty<Style, BackgroundRepeat>;
|
||||
export const backgroundSizeProperty: CssProperty<Style, string>;
|
||||
export const backgroundPositionProperty: CssProperty<Style, string>;
|
||||
|
||||
export const borderColorProperty: ShorthandProperty<Style, string | Color>;
|
||||
export const borderTopColorProperty: CssProperty<Style, Color>;
|
||||
export const borderRightColorProperty: CssProperty<Style, Color>;
|
||||
export const borderBottomColorProperty: CssProperty<Style, Color>;
|
||||
export const borderLeftColorProperty: CssProperty<Style, Color>;
|
||||
|
||||
export const borderWidthProperty: ShorthandProperty<Style, string | Length>;
|
||||
export const borderTopWidthProperty: CssProperty<Style, Length>;
|
||||
export const borderRightWidthProperty: CssProperty<Style, Length>;
|
||||
export const borderBottomWidthProperty: CssProperty<Style, Length>;
|
||||
export const borderLeftWidthProperty: CssProperty<Style, Length>;
|
||||
|
||||
export const borderRadiusProperty: ShorthandProperty<Style, string | Length>;
|
||||
export const borderTopLeftRadiusProperty: CssProperty<Style, Length>;
|
||||
export const borderTopRightRadiusProperty: CssProperty<Style, Length>;
|
||||
export const borderBottomRightRadiusProperty: CssProperty<Style, Length>;
|
||||
export const borderBottomLeftRadiusProperty: CssProperty<Style, Length>;
|
||||
|
||||
export const zIndexProperty: CssProperty<Style, number>;
|
||||
export const visibilityProperty: CssProperty<Style, Visibility>;
|
||||
export const opacityProperty: CssAnimationProperty<Style, number>;
|
||||
|
||||
export const minWidthProperty: CssProperty<Style, dip | LengthDipUnit | LengthPxUnit>;
|
||||
export const minHeightProperty: CssProperty<Style, dip | LengthDipUnit | LengthPxUnit>;
|
||||
export const widthProperty: CssAnimationProperty<Style, PercentLength>;
|
||||
export const heightProperty: CssAnimationProperty<Style, PercentLength>;
|
||||
export const lineHeightProperty: CssProperty<Style, number>;
|
||||
export const marginProperty: ShorthandProperty<Style, string | PercentLength>;
|
||||
export const marginLeftProperty: CssProperty<Style, PercentLength>;
|
||||
export const marginRightProperty: CssProperty<Style, PercentLength>;
|
||||
export const marginTopProperty: CssProperty<Style, PercentLength>;
|
||||
export const marginBottomProperty: CssProperty<Style, PercentLength>;
|
||||
|
||||
export const paddingProperty: ShorthandProperty<Style, string | Length>;
|
||||
export const paddingLeftProperty: CssProperty<Style, Length>;
|
||||
export const paddingRightProperty: CssProperty<Style, Length>;
|
||||
export const paddingTopProperty: CssProperty<Style, Length>;
|
||||
export const paddingBottomProperty: CssProperty<Style, Length>;
|
||||
|
||||
export const horizontalAlignmentProperty: CssProperty<Style, HorizontalAlignment>;
|
||||
export const verticalAlignmentProperty: CssProperty<Style, VerticalAlignment>;
|
||||
|
||||
export const fontSizeProperty: InheritedCssProperty<Style, number>;
|
||||
export const fontFamilyProperty: InheritedCssProperty<Style, string>;
|
||||
export const fontStyleProperty: InheritedCssProperty<Style, FontStyle>;
|
||||
export const fontWeightProperty: InheritedCssProperty<Style, FontWeight>;
|
||||
|
||||
export const backgroundInternalProperty: CssProperty<Style, Background>;
|
||||
export const fontInternalProperty: InheritedCssProperty<Style, Font>;
|
||||
|
||||
export type BackgroundRepeat = "repeat" | "repeat-x" | "repeat-y" | "no-repeat";
|
||||
export type Visibility = "visible" | "hidden" | "collapse";
|
||||
export type HorizontalAlignment = "left" | "center" | "right" | "stretch";
|
||||
export type VerticalAlignment = "top" | "middle" | "bottom" | "stretch";
|
||||
1259
nativescript-core/ui/styling/style-properties.ts
Normal file
1259
nativescript-core/ui/styling/style-properties.ts
Normal file
File diff suppressed because it is too large
Load Diff
54
nativescript-core/ui/styling/style-scope.d.ts
vendored
Normal file
54
nativescript-core/ui/styling/style-scope.d.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @module "ui/styling/style-scope"
|
||||
* @private
|
||||
*/ /** */
|
||||
|
||||
//@private
|
||||
import { ViewBase } from "../core/view-base";
|
||||
import { SyntaxTree } from "../../css";
|
||||
import { RuleSet, Node, SelectorCore, ChangeMap } from "../styling/css-selector";
|
||||
import { KeyframeAnimationInfo } from "../animation/keyframe-animation";
|
||||
|
||||
export class CssState {
|
||||
/**
|
||||
* Re-evaluate the selectors and apply any changes to the underlying view.
|
||||
*/
|
||||
public apply(): void;
|
||||
|
||||
/**
|
||||
* Gets the static selectors that match the view and the dynamic selectors that may potentially match the view.
|
||||
*/
|
||||
public changeMap: ChangeMap<ViewBase>;
|
||||
|
||||
/**
|
||||
* Checks whether style scope and CSS state selectors are in sync.
|
||||
*/
|
||||
public isSelectorsLatestVersionApplied(): boolean
|
||||
}
|
||||
|
||||
export class StyleScope {
|
||||
public css: string;
|
||||
public addCss(cssString: string, cssFileName?: string): void;
|
||||
public addCssFile(cssFileName: string): void;
|
||||
public changeCssFile(cssFileName: string): void;
|
||||
|
||||
public static createSelectorsFromCss(css: string, cssFileName: string, keyframes: Object): RuleSet[];
|
||||
public static createSelectorsFromImports(tree: SyntaxTree, keyframes: Object): RuleSet[];
|
||||
public ensureSelectors(): number;
|
||||
|
||||
/**
|
||||
* Increase the application CSS selector version.
|
||||
*/
|
||||
public _increaseApplicationCssSelectorVersion(): void;
|
||||
public isApplicationCssSelectorsLatestVersionApplied(): boolean;
|
||||
public isLocalCssSelectorsLatestVersionApplied(): boolean;
|
||||
|
||||
public applySelectors(view: ViewBase): void
|
||||
public query(options: Node): SelectorCore[];
|
||||
|
||||
public getKeyframeAnimationWithName(animationName: string): KeyframeAnimationInfo;
|
||||
public getAnimations(ruleset: RuleSet): KeyframeAnimationInfo[];
|
||||
}
|
||||
|
||||
export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (string) => boolean, importSource?: string): string;
|
||||
export function applyInlineStyle(view: ViewBase, style: string): void;
|
||||
949
nativescript-core/ui/styling/style-scope.ts
Normal file
949
nativescript-core/ui/styling/style-scope.ts
Normal file
@@ -0,0 +1,949 @@
|
||||
import { Keyframes } from "../animation/keyframe-animation";
|
||||
import { ViewBase } from "../core/view-base";
|
||||
import { View } from "../core/view";
|
||||
import { unsetValue, _evaluateCssVariableExpression, _evaluateCssCalcExpression, isCssVariable, isCssVariableExpression, isCssCalcExpression } from "../core/properties";
|
||||
import {
|
||||
SyntaxTree,
|
||||
Keyframes as KeyframesDefinition,
|
||||
parse as parseCss,
|
||||
Node as CssNode,
|
||||
} from "../../css";
|
||||
import {
|
||||
CSS3Parser,
|
||||
CSSNativeScript
|
||||
} from "../../css/parser";
|
||||
import {
|
||||
cssTreeParse
|
||||
} from "../../css/css-tree-parser";
|
||||
|
||||
import {
|
||||
RuleSet,
|
||||
SelectorsMap,
|
||||
SelectorCore,
|
||||
SelectorsMatch,
|
||||
ChangeMap,
|
||||
fromAstNodes,
|
||||
Node,
|
||||
} from "./css-selector";
|
||||
import {
|
||||
write as traceWrite,
|
||||
categories as traceCategories,
|
||||
messageType as traceMessageType,
|
||||
} from "../../trace";
|
||||
import { File, knownFolders, path } from "../../file-system";
|
||||
import * as application from "../../application";
|
||||
import { profile } from "../../profiling";
|
||||
|
||||
import * as kam from "../animation/keyframe-animation";
|
||||
let keyframeAnimationModule: typeof kam;
|
||||
function ensureKeyframeAnimationModule() {
|
||||
if (!keyframeAnimationModule) {
|
||||
keyframeAnimationModule = require("../animation/keyframe-animation");
|
||||
}
|
||||
}
|
||||
|
||||
import * as capm from "./css-animation-parser";
|
||||
import { sanitizeModuleName } from "../builder/module-name-sanitizer";
|
||||
import { resolveModuleName } from "../../module-name-resolver";
|
||||
|
||||
let cssAnimationParserModule: typeof capm;
|
||||
function ensureCssAnimationParserModule() {
|
||||
if (!cssAnimationParserModule) {
|
||||
cssAnimationParserModule = require("./css-animation-parser");
|
||||
}
|
||||
}
|
||||
|
||||
let parser: "rework" | "nativescript" | "css-tree" = "css-tree";
|
||||
try {
|
||||
const appConfig = require("~/package.json");
|
||||
if (appConfig) {
|
||||
if (appConfig.cssParser === "rework") {
|
||||
parser = "rework";
|
||||
} else if (appConfig.cssParser === "nativescript") {
|
||||
parser = "nativescript";
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate css-variable and css-calc expressions
|
||||
*/
|
||||
function evaluateCssExpressions(view: ViewBase, property: string, value: string) {
|
||||
const newValue = _evaluateCssVariableExpression(view, property, value);
|
||||
if (newValue === "unset") {
|
||||
return unsetValue;
|
||||
}
|
||||
|
||||
value = newValue;
|
||||
|
||||
try {
|
||||
value = _evaluateCssCalcExpression(value);
|
||||
} catch (e) {
|
||||
traceWrite(`Failed to evaluate css-calc for property [${property}] for expression [${value}] to ${view}. ${e.stack}`, traceCategories.Error, traceMessageType.error);
|
||||
|
||||
return unsetValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function mergeCssSelectors(): void {
|
||||
applicationCssSelectors = applicationSelectors.slice();
|
||||
applicationCssSelectors.push.apply(applicationCssSelectors, applicationAdditionalSelectors);
|
||||
applicationCssSelectorVersion++;
|
||||
}
|
||||
|
||||
let applicationCssSelectors: RuleSet[] = [];
|
||||
let applicationCssSelectorVersion: number = 0;
|
||||
let applicationSelectors: RuleSet[] = [];
|
||||
const applicationAdditionalSelectors: RuleSet[] = [];
|
||||
const applicationKeyframes: any = {};
|
||||
const animationsSymbol: symbol = Symbol("animations");
|
||||
const pattern: RegExp = /('|")(.*?)\1/;
|
||||
|
||||
class CSSSource {
|
||||
private _selectors: RuleSet[] = [];
|
||||
|
||||
private constructor(private _ast: SyntaxTree, private _url: string, private _file: string, private _keyframes: KeyframesMap, private _source: string) {
|
||||
this.parse();
|
||||
}
|
||||
|
||||
public static fromURI(uri: string, keyframes: KeyframesMap): CSSSource {
|
||||
// webpack modules require all file paths to be relative to /app folder
|
||||
const appRelativeUri = CSSSource.pathRelativeToApp(uri);
|
||||
const sanitizedModuleName = sanitizeModuleName(appRelativeUri);
|
||||
const resolvedModuleName = resolveModuleName(sanitizedModuleName, "css");
|
||||
|
||||
try {
|
||||
const cssOrAst = global.loadModule(resolvedModuleName, true);
|
||||
if (cssOrAst) {
|
||||
if (typeof cssOrAst === "string") {
|
||||
// raw-loader
|
||||
return CSSSource.fromSource(cssOrAst, keyframes, resolvedModuleName);
|
||||
} else if (typeof cssOrAst === "object" && cssOrAst.type === "stylesheet" && cssOrAst.stylesheet && cssOrAst.stylesheet.rules) {
|
||||
// css-loader
|
||||
return CSSSource.fromAST(cssOrAst, keyframes, resolvedModuleName);
|
||||
} else {
|
||||
// css2json-loader
|
||||
return CSSSource.fromSource(cssOrAst.toString(), keyframes, resolvedModuleName);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
traceWrite(`Could not load CSS from ${uri}: ${e}`, traceCategories.Error, traceMessageType.error);
|
||||
}
|
||||
|
||||
return CSSSource.fromFile(appRelativeUri, keyframes);
|
||||
}
|
||||
|
||||
private static pathRelativeToApp(uri: string): string {
|
||||
if (!uri.startsWith("/")) {
|
||||
return uri;
|
||||
}
|
||||
|
||||
const appPath = knownFolders.currentApp().path;
|
||||
if (!uri.startsWith(appPath)) {
|
||||
traceWrite(`${uri} does not start with ${appPath}`, traceCategories.Error, traceMessageType.error);
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
const relativeUri = `.${uri.substr(appPath.length)}`;
|
||||
|
||||
return relativeUri;
|
||||
}
|
||||
|
||||
public static fromFile(url: string, keyframes: KeyframesMap): CSSSource {
|
||||
// .scss, .sass, etc. css files in vanilla app are usually compiled to .css so we will try to load a compiled file first.
|
||||
let cssFileUrl = url.replace(/\..\w+$/, ".css");
|
||||
if (cssFileUrl !== url) {
|
||||
const cssFile = CSSSource.resolveCSSPathFromURL(cssFileUrl);
|
||||
if (cssFile) {
|
||||
return new CSSSource(undefined, url, cssFile, keyframes, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const file = CSSSource.resolveCSSPathFromURL(url);
|
||||
|
||||
return new CSSSource(undefined, url, file, keyframes, undefined);
|
||||
}
|
||||
|
||||
public static fromFileImport(url: string, keyframes: KeyframesMap, importSource: string): CSSSource {
|
||||
const file = CSSSource.resolveCSSPathFromURL(url, importSource);
|
||||
|
||||
return new CSSSource(undefined, url, file, keyframes, undefined);
|
||||
}
|
||||
|
||||
@profile
|
||||
public static resolveCSSPathFromURL(url: string, importSource?: string): string {
|
||||
const app = knownFolders.currentApp().path;
|
||||
const file = resolveFileNameFromUrl(url, app, File.exists, importSource);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
public static fromSource(source: string, keyframes: KeyframesMap, url?: string): CSSSource {
|
||||
return new CSSSource(undefined, url, undefined, keyframes, source);
|
||||
}
|
||||
|
||||
public static fromAST(ast: SyntaxTree, keyframes: KeyframesMap, url?: string): CSSSource {
|
||||
return new CSSSource(ast, url, undefined, keyframes, undefined);
|
||||
}
|
||||
|
||||
get selectors(): RuleSet[] { return this._selectors; }
|
||||
get source(): string { return this._source; }
|
||||
|
||||
@profile
|
||||
private load(): void {
|
||||
const file = File.fromPath(this._file);
|
||||
this._source = file.readTextSync();
|
||||
}
|
||||
|
||||
@profile
|
||||
private parse(): void {
|
||||
try {
|
||||
if (!this._ast) {
|
||||
if (!this._source && this._file) {
|
||||
this.load();
|
||||
}
|
||||
// [object Object] check guards against empty app.css file
|
||||
if (this._source && this.source !== "[object Object]") {
|
||||
this.parseCSSAst();
|
||||
}
|
||||
}
|
||||
if (this._ast) {
|
||||
this.createSelectors();
|
||||
} else {
|
||||
this._selectors = [];
|
||||
}
|
||||
} catch (e) {
|
||||
traceWrite("Css styling failed: " + e, traceCategories.Error, traceMessageType.error);
|
||||
this._selectors = [];
|
||||
}
|
||||
}
|
||||
|
||||
@profile
|
||||
private parseCSSAst() {
|
||||
if (this._source) {
|
||||
switch (parser) {
|
||||
case "css-tree":
|
||||
this._ast = cssTreeParse(this._source, this._file);
|
||||
|
||||
return;
|
||||
case "nativescript":
|
||||
const cssparser = new CSS3Parser(this._source);
|
||||
const stylesheet = cssparser.parseAStylesheet();
|
||||
const cssNS = new CSSNativeScript();
|
||||
this._ast = cssNS.parseStylesheet(stylesheet);
|
||||
|
||||
return;
|
||||
case "rework":
|
||||
this._ast = parseCss(this._source, { source: this._file });
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@profile
|
||||
private createSelectors() {
|
||||
if (this._ast) {
|
||||
this._selectors = [
|
||||
...this.createSelectorsFromImports(),
|
||||
...this.createSelectorsFromSyntaxTree()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private createSelectorsFromImports(): RuleSet[] {
|
||||
const imports = this._ast["stylesheet"]["rules"].filter(r => r.type === "import");
|
||||
|
||||
const urlFromImportObject = importObject => {
|
||||
const importItem = importObject["import"] as string;
|
||||
const urlMatch = importItem && importItem.match(pattern);
|
||||
|
||||
return urlMatch && urlMatch[2];
|
||||
};
|
||||
|
||||
const sourceFromImportObject = importObject =>
|
||||
importObject["position"] && importObject["position"]["source"];
|
||||
|
||||
const toUrlSourcePair = importObject => ({
|
||||
url: urlFromImportObject(importObject),
|
||||
source: sourceFromImportObject(importObject),
|
||||
});
|
||||
|
||||
const getCssFile = ({ url, source }) => source ?
|
||||
CSSSource.fromFileImport(url, this._keyframes, source) :
|
||||
CSSSource.fromURI(url, this._keyframes);
|
||||
|
||||
const cssFiles = imports
|
||||
.map(toUrlSourcePair)
|
||||
.filter(({ url }) => !!url)
|
||||
.map(getCssFile);
|
||||
|
||||
const selectors = cssFiles.map(file => (file && file.selectors) || []);
|
||||
|
||||
return selectors.reduce((acc, val) => acc.concat(val), []);
|
||||
}
|
||||
|
||||
private createSelectorsFromSyntaxTree(): RuleSet[] {
|
||||
const nodes = this._ast.stylesheet.rules;
|
||||
(<KeyframesDefinition[]>nodes.filter(isKeyframe)).forEach(node => this._keyframes[node.name] = node);
|
||||
|
||||
const rulesets = fromAstNodes(nodes);
|
||||
if (rulesets && rulesets.length) {
|
||||
ensureCssAnimationParserModule();
|
||||
|
||||
rulesets.forEach(rule => {
|
||||
rule[animationsSymbol] = cssAnimationParserModule.CssAnimationParser
|
||||
.keyframeAnimationsFromCSSDeclarations(rule.declarations);
|
||||
});
|
||||
}
|
||||
|
||||
return rulesets;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this._file || this._url || "(in-memory)";
|
||||
}
|
||||
}
|
||||
|
||||
export function removeTaggedAdditionalCSS(tag: String | Number): Boolean {
|
||||
let changed = false;
|
||||
for (let i = 0; i < applicationAdditionalSelectors.length; i++) {
|
||||
if (applicationAdditionalSelectors[i].tag === tag) {
|
||||
applicationAdditionalSelectors.splice(i, 1);
|
||||
i--;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) { mergeCssSelectors(); }
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
export function addTaggedAdditionalCSS(cssText: string, tag?: string | Number): Boolean {
|
||||
const parsed: RuleSet[] = CSSSource.fromSource(cssText, applicationKeyframes, undefined).selectors;
|
||||
let changed = false;
|
||||
if (parsed && parsed.length) {
|
||||
changed = true;
|
||||
if (tag != null) {
|
||||
for (let i = 0; i < parsed.length; i++) {
|
||||
parsed[i].tag = tag;
|
||||
}
|
||||
}
|
||||
applicationAdditionalSelectors.push.apply(applicationAdditionalSelectors, parsed);
|
||||
mergeCssSelectors();
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
const onCssChanged = profile("\"style-scope\".onCssChanged", (args: application.CssChangedEventData) => {
|
||||
if (args.cssText) {
|
||||
const parsed = CSSSource.fromSource(args.cssText, applicationKeyframes, args.cssFile).selectors;
|
||||
if (parsed) {
|
||||
applicationAdditionalSelectors.push.apply(applicationAdditionalSelectors, parsed);
|
||||
mergeCssSelectors();
|
||||
}
|
||||
} else if (args.cssFile) {
|
||||
loadCss(args.cssFile);
|
||||
}
|
||||
});
|
||||
|
||||
function onLiveSync(args: application.CssChangedEventData): void {
|
||||
loadCss(application.getCssFileName());
|
||||
}
|
||||
|
||||
const loadCss = profile(`"style-scope".loadCss`, (cssModule: string) => {
|
||||
if (!cssModule) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// safely remove "./" as global CSS should be resolved relative to app folder
|
||||
if (cssModule.startsWith("./")) {
|
||||
cssModule = cssModule.substr(2);
|
||||
}
|
||||
|
||||
const result = CSSSource.fromURI(cssModule, applicationKeyframes).selectors;
|
||||
if (result.length > 0) {
|
||||
applicationSelectors = result;
|
||||
mergeCssSelectors();
|
||||
}
|
||||
});
|
||||
|
||||
application.on("cssChanged", onCssChanged);
|
||||
application.on("livesync", onLiveSync);
|
||||
|
||||
// Call to this method is injected in the application in:
|
||||
// - no-snapshot - code injected in app.ts by [bundle-config-loader](https://github.com/NativeScript/nativescript-dev-webpack/blob/9b1e34d8ef838006c9b575285c42d2304f5f02b5/bundle-config-loader.ts#L85-L92)
|
||||
// - with-snapshot - code injected in snapshot bundle by [NativeScriptSnapshotPlugin](https://github.com/NativeScript/nativescript-dev-webpack/blob/48b26f412fd70c19dc0b9c7763e08e9505a0ae11/plugins/NativeScriptSnapshotPlugin/index.js#L48-L56)
|
||||
// Having the app.css loaded in snapshot provides significant boost in startup (when using the ns-theme ~150 ms). However, because app.css is resolved at build-time,
|
||||
// when the snapshot is created - there is no way to use file qualifiers or change the name of on app.css
|
||||
export const loadAppCSS = profile("\"style-scope\".loadAppCSS", (args: application.LoadAppCSSEventData) => {
|
||||
loadCss(args.cssFile);
|
||||
application.off("loadAppCss", loadAppCSS);
|
||||
});
|
||||
|
||||
if (application.hasLaunched()) {
|
||||
loadAppCSS({ eventName: "loadAppCss", object: <any>application, cssFile: application.getCssFileName() });
|
||||
} else {
|
||||
application.on("loadAppCss", loadAppCSS);
|
||||
}
|
||||
|
||||
export class CssState {
|
||||
static emptyChangeMap: Readonly<ChangeMap<ViewBase>> = Object.freeze(new Map());
|
||||
static emptyPropertyBag: Readonly<{}> = Object.freeze({});
|
||||
static emptyAnimationArray: ReadonlyArray<kam.KeyframeAnimation> = Object.freeze([]);
|
||||
static emptyMatch: Readonly<SelectorsMatch<ViewBase>> = { selectors: [], changeMap: new Map() };
|
||||
|
||||
_onDynamicStateChangeHandler: () => void;
|
||||
_appliedChangeMap: Readonly<ChangeMap<ViewBase>>;
|
||||
_appliedPropertyValues: Readonly<{}>;
|
||||
_appliedAnimations: ReadonlyArray<kam.KeyframeAnimation>;
|
||||
_appliedSelectorsVersion: number;
|
||||
|
||||
_match: SelectorsMatch<ViewBase>;
|
||||
_matchInvalid: boolean;
|
||||
_playsKeyframeAnimations: boolean;
|
||||
|
||||
constructor(private viewRef: WeakRef<ViewBase>) {
|
||||
this._onDynamicStateChangeHandler = () => this.updateDynamicState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a change had occurred that may invalidate the statically matching selectors (class, id, ancestor selectors).
|
||||
* As a result, at some point in time, the selectors matched have to be requerried from the style scope and applied to the view.
|
||||
*/
|
||||
public onChange(): void {
|
||||
const view = this.viewRef.get();
|
||||
if (view && view.isLoaded) {
|
||||
this.unsubscribeFromDynamicUpdates();
|
||||
this.updateMatch();
|
||||
this.subscribeForDynamicUpdates();
|
||||
this.updateDynamicState();
|
||||
} else {
|
||||
this._matchInvalid = true;
|
||||
}
|
||||
}
|
||||
|
||||
public isSelectorsLatestVersionApplied(): boolean {
|
||||
const view = this.viewRef.get();
|
||||
if (!view) {
|
||||
traceWrite(`isSelectorsLatestVersionApplied returns default value "false" because "this.viewRef" cleared.`, traceCategories.Style, traceMessageType.warn);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.viewRef.get()._styleScope._getSelectorsVersion() === this._appliedSelectorsVersion;
|
||||
}
|
||||
|
||||
public onLoaded(): void {
|
||||
if (this._matchInvalid) {
|
||||
this.updateMatch();
|
||||
}
|
||||
this.subscribeForDynamicUpdates();
|
||||
this.updateDynamicState();
|
||||
}
|
||||
|
||||
public onUnloaded(): void {
|
||||
this.unsubscribeFromDynamicUpdates();
|
||||
}
|
||||
|
||||
@profile
|
||||
private updateMatch() {
|
||||
const view = this.viewRef.get();
|
||||
if (view && view._styleScope) {
|
||||
this._match = view._styleScope.matchSelectors(view);
|
||||
this._appliedSelectorsVersion = view._styleScope._getSelectorsVersion();
|
||||
} else {
|
||||
this._match = CssState.emptyMatch;
|
||||
}
|
||||
|
||||
this._matchInvalid = false;
|
||||
}
|
||||
|
||||
@profile
|
||||
private updateDynamicState(): void {
|
||||
const view = this.viewRef.get();
|
||||
if (!view) {
|
||||
traceWrite(`updateDynamicState not executed to view because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingSelectors = this._match.selectors.filter(sel => sel.dynamic ? sel.match(view) : true);
|
||||
view._batchUpdate(() => {
|
||||
this.stopKeyframeAnimations();
|
||||
this.setPropertyValues(matchingSelectors);
|
||||
this.playKeyframeAnimations(matchingSelectors);
|
||||
});
|
||||
}
|
||||
|
||||
private playKeyframeAnimations(matchingSelectors: SelectorCore[]): void {
|
||||
const animations: kam.KeyframeAnimation[] = [];
|
||||
|
||||
matchingSelectors.forEach(selector => {
|
||||
let ruleAnimations: kam.KeyframeAnimationInfo[] = selector.ruleset[animationsSymbol];
|
||||
if (ruleAnimations) {
|
||||
ensureKeyframeAnimationModule();
|
||||
for (let animationInfo of ruleAnimations) {
|
||||
let animation = keyframeAnimationModule.KeyframeAnimation.keyframeAnimationFromInfo(animationInfo);
|
||||
if (animation) {
|
||||
animations.push(animation);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this._playsKeyframeAnimations = animations.length > 0) {
|
||||
const view = this.viewRef.get();
|
||||
if (!view) {
|
||||
traceWrite(`KeyframeAnimations cannot play because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
animations.map(animation => animation.play(<View>view));
|
||||
Object.freeze(animations);
|
||||
this._appliedAnimations = animations;
|
||||
}
|
||||
}
|
||||
|
||||
private stopKeyframeAnimations(): void {
|
||||
if (!this._playsKeyframeAnimations) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._appliedAnimations
|
||||
.filter(animation => animation.isPlaying)
|
||||
.forEach(animation => animation.cancel());
|
||||
this._appliedAnimations = CssState.emptyAnimationArray;
|
||||
|
||||
const view = this.viewRef.get();
|
||||
if (view) {
|
||||
view.style["keyframe:rotate"] = unsetValue;
|
||||
view.style["keyframe:rotateX"] = unsetValue;
|
||||
view.style["keyframe:rotateY"] = unsetValue;
|
||||
view.style["keyframe:scaleX"] = unsetValue;
|
||||
view.style["keyframe:scaleY"] = unsetValue;
|
||||
view.style["keyframe:translateX"] = unsetValue;
|
||||
view.style["keyframe:translateY"] = unsetValue;
|
||||
view.style["keyframe:backgroundColor"] = unsetValue;
|
||||
view.style["keyframe:opacity"] = unsetValue;
|
||||
} else {
|
||||
traceWrite(`KeyframeAnimations cannot be stopped because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn);
|
||||
}
|
||||
|
||||
this._playsKeyframeAnimations = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the difference between the previously applied property values,
|
||||
* and the new set of property values that have to be applied for the provided selectors.
|
||||
* Apply the values and ensure each property setter is called at most once to avoid excessive change notifications.
|
||||
* @param matchingSelectors
|
||||
*/
|
||||
private setPropertyValues(matchingSelectors: SelectorCore[]): void {
|
||||
const view = this.viewRef.get();
|
||||
if (!view) {
|
||||
traceWrite(`${matchingSelectors} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newPropertyValues = new view.style.PropertyBag();
|
||||
matchingSelectors.forEach(selector =>
|
||||
selector.ruleset.declarations.forEach(declaration =>
|
||||
newPropertyValues[declaration.property] = declaration.value));
|
||||
|
||||
const oldProperties = this._appliedPropertyValues;
|
||||
|
||||
let isCssExpressionInUse = false;
|
||||
|
||||
// Update values for the scope's css-variables
|
||||
view.style.resetScopedCssVariables();
|
||||
|
||||
for (const property in newPropertyValues) {
|
||||
const value = newPropertyValues[property];
|
||||
if (isCssVariable(property)) {
|
||||
view.style.setScopedCssVariable(property, value);
|
||||
|
||||
delete newPropertyValues[property];
|
||||
continue;
|
||||
}
|
||||
|
||||
isCssExpressionInUse = isCssExpressionInUse || isCssVariableExpression(value) || isCssCalcExpression(value);
|
||||
}
|
||||
|
||||
if (isCssExpressionInUse) {
|
||||
// Evalute css-expressions to get the latest values.
|
||||
for (const property in newPropertyValues) {
|
||||
const value = evaluateCssExpressions(view, property, newPropertyValues[property]);
|
||||
if (value === unsetValue) {
|
||||
delete newPropertyValues[property];
|
||||
continue;
|
||||
}
|
||||
|
||||
newPropertyValues[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Property values are fully updated, freeze the object to be used for next update.
|
||||
Object.freeze(newPropertyValues);
|
||||
|
||||
// Unset removed values
|
||||
for (const property in oldProperties) {
|
||||
if (!(property in newPropertyValues)) {
|
||||
if (property in view.style) {
|
||||
view.style[`css:${property}`] = unsetValue;
|
||||
} else {
|
||||
// TRICKY: How do we unset local value?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set new values to the style
|
||||
for (const property in newPropertyValues) {
|
||||
if (oldProperties && property in oldProperties && oldProperties[property] === newPropertyValues[property]) {
|
||||
// Skip unchanged values
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = newPropertyValues[property];
|
||||
try {
|
||||
if (property in view.style) {
|
||||
view.style[`css:${property}`] = value;
|
||||
} else {
|
||||
const camelCasedProperty = property.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
|
||||
view[camelCasedProperty] = value;
|
||||
}
|
||||
} catch (e) {
|
||||
traceWrite(`Failed to apply property [${property}] with value [${value}] to ${view}. ${e.stack}`, traceCategories.Error, traceMessageType.error);
|
||||
}
|
||||
}
|
||||
|
||||
this._appliedPropertyValues = newPropertyValues;
|
||||
}
|
||||
|
||||
private subscribeForDynamicUpdates(): void {
|
||||
const changeMap = this._match.changeMap;
|
||||
changeMap.forEach((changes, view) => {
|
||||
if (changes.attributes) {
|
||||
changes.attributes.forEach(attribute => {
|
||||
view.addEventListener(attribute + "Change", this._onDynamicStateChangeHandler);
|
||||
});
|
||||
}
|
||||
if (changes.pseudoClasses) {
|
||||
changes.pseudoClasses.forEach(pseudoClass => {
|
||||
let eventName = ":" + pseudoClass;
|
||||
view.addEventListener(":" + pseudoClass, this._onDynamicStateChangeHandler);
|
||||
if (view[eventName]) {
|
||||
view[eventName](+1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this._appliedChangeMap = changeMap;
|
||||
}
|
||||
|
||||
private unsubscribeFromDynamicUpdates(): void {
|
||||
this._appliedChangeMap.forEach((changes, view) => {
|
||||
if (changes.attributes) {
|
||||
changes.attributes.forEach(attribute => {
|
||||
view.removeEventListener(attribute + "Change", this._onDynamicStateChangeHandler);
|
||||
});
|
||||
}
|
||||
if (changes.pseudoClasses) {
|
||||
changes.pseudoClasses.forEach(pseudoClass => {
|
||||
let eventName = ":" + pseudoClass;
|
||||
view.removeEventListener(eventName, this._onDynamicStateChangeHandler);
|
||||
if (view[eventName]) {
|
||||
view[eventName](-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this._appliedChangeMap = CssState.emptyChangeMap;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const view = this.viewRef.get();
|
||||
if (!view) {
|
||||
traceWrite(`toString() of CssState cannot execute correctly because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${view}._cssState`;
|
||||
}
|
||||
}
|
||||
CssState.prototype._appliedChangeMap = CssState.emptyChangeMap;
|
||||
CssState.prototype._appliedPropertyValues = CssState.emptyPropertyBag;
|
||||
CssState.prototype._appliedAnimations = CssState.emptyAnimationArray;
|
||||
CssState.prototype._matchInvalid = true;
|
||||
|
||||
export class StyleScope {
|
||||
|
||||
private _selectors: SelectorsMap;
|
||||
private _css: string = "";
|
||||
private _mergedCssSelectors: RuleSet[];
|
||||
private _localCssSelectors: RuleSet[] = [];
|
||||
private _localCssSelectorVersion: number = 0;
|
||||
private _localCssSelectorsAppliedVersion: number = 0;
|
||||
private _applicationCssSelectorsAppliedVersion: number = 0;
|
||||
private _keyframes = new Map<string, Keyframes>();
|
||||
|
||||
get css(): string {
|
||||
return this._css;
|
||||
}
|
||||
|
||||
set css(value: string) {
|
||||
this.setCss(value);
|
||||
}
|
||||
|
||||
public addCss(cssString: string, cssFileName?: string): void {
|
||||
this.appendCss(cssString, cssFileName);
|
||||
}
|
||||
|
||||
public addCssFile(cssFileName: string): void {
|
||||
this.appendCss(null, cssFileName);
|
||||
}
|
||||
|
||||
public changeCssFile(cssFileName: string): void {
|
||||
if (!cssFileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cssSelectors = CSSSource.fromURI(cssFileName, this._keyframes);
|
||||
this._css = cssSelectors.source;
|
||||
this._localCssSelectors = cssSelectors.selectors;
|
||||
this._localCssSelectorVersion++;
|
||||
this.ensureSelectors();
|
||||
}
|
||||
|
||||
@profile
|
||||
private setCss(cssString: string, cssFileName?): void {
|
||||
this._css = cssString;
|
||||
|
||||
const cssFile = CSSSource.fromSource(cssString, this._keyframes, cssFileName);
|
||||
this._localCssSelectors = cssFile.selectors;
|
||||
this._localCssSelectorVersion++;
|
||||
this.ensureSelectors();
|
||||
}
|
||||
|
||||
@profile
|
||||
private appendCss(cssString: string, cssFileName?): void {
|
||||
if (!cssString && !cssFileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsedCssSelectors = cssString ? CSSSource.fromSource(cssString, this._keyframes, cssFileName) : CSSSource.fromURI(cssFileName, this._keyframes);
|
||||
this._css = this._css + parsedCssSelectors.source;
|
||||
this._localCssSelectors.push.apply(this._localCssSelectors, parsedCssSelectors.selectors);
|
||||
this._localCssSelectorVersion++;
|
||||
this.ensureSelectors();
|
||||
}
|
||||
|
||||
public getKeyframeAnimationWithName(animationName: string): kam.KeyframeAnimationInfo {
|
||||
const cssKeyframes = this._keyframes[animationName];
|
||||
if (!cssKeyframes) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureKeyframeAnimationModule();
|
||||
const animation = new keyframeAnimationModule.KeyframeAnimationInfo();
|
||||
ensureCssAnimationParserModule();
|
||||
animation.keyframes = cssAnimationParserModule
|
||||
.CssAnimationParser.keyframesArrayFromCSS(cssKeyframes.keyframes);
|
||||
|
||||
return animation;
|
||||
}
|
||||
|
||||
public ensureSelectors(): number {
|
||||
if (!this.isApplicationCssSelectorsLatestVersionApplied() ||
|
||||
!this.isLocalCssSelectorsLatestVersionApplied() ||
|
||||
!this._mergedCssSelectors) {
|
||||
|
||||
this._createSelectors();
|
||||
}
|
||||
|
||||
return this._getSelectorsVersion();
|
||||
}
|
||||
|
||||
public _increaseApplicationCssSelectorVersion(): void {
|
||||
applicationCssSelectorVersion++;
|
||||
}
|
||||
|
||||
public isApplicationCssSelectorsLatestVersionApplied(): boolean {
|
||||
return this._applicationCssSelectorsAppliedVersion === applicationCssSelectorVersion;
|
||||
}
|
||||
|
||||
public isLocalCssSelectorsLatestVersionApplied(): boolean {
|
||||
return this._localCssSelectorsAppliedVersion === this._localCssSelectorVersion;
|
||||
}
|
||||
|
||||
@profile
|
||||
private _createSelectors() {
|
||||
let toMerge: RuleSet[][] = [];
|
||||
toMerge.push(applicationCssSelectors);
|
||||
this._applicationCssSelectorsAppliedVersion = applicationCssSelectorVersion;
|
||||
toMerge.push(this._localCssSelectors);
|
||||
this._localCssSelectorsAppliedVersion = this._localCssSelectorVersion;
|
||||
for (let keyframe in applicationKeyframes) {
|
||||
this._keyframes[keyframe] = applicationKeyframes[keyframe];
|
||||
}
|
||||
|
||||
if (toMerge.length > 0) {
|
||||
this._mergedCssSelectors = toMerge.filter(m => !!m).reduce((merged, next) => merged.concat(next), []);
|
||||
this._applyKeyframesOnSelectors();
|
||||
this._selectors = new SelectorsMap(this._mergedCssSelectors);
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: This @profile decorator creates a circular dependency
|
||||
// HACK: because the function parameter type is evaluated with 'typeof'
|
||||
@profile
|
||||
public matchSelectors(view: any): SelectorsMatch<ViewBase> { // should be (view: ViewBase): SelectorsMatch<ViewBase>
|
||||
this.ensureSelectors();
|
||||
|
||||
return this._selectors.query(view);
|
||||
}
|
||||
|
||||
public query(node: Node): SelectorCore[] {
|
||||
this.ensureSelectors();
|
||||
|
||||
return this._selectors.query(node).selectors;
|
||||
}
|
||||
|
||||
private _getSelectorsVersion() {
|
||||
// The counters can only go up. So we can return just appVersion + localVersion
|
||||
// The 100000 * appVersion is just for easier debugging
|
||||
return 100000 * this._applicationCssSelectorsAppliedVersion + this._localCssSelectorsAppliedVersion;
|
||||
}
|
||||
|
||||
private _applyKeyframesOnSelectors() {
|
||||
for (let i = this._mergedCssSelectors.length - 1; i >= 0; i--) {
|
||||
let ruleset = this._mergedCssSelectors[i];
|
||||
let animations: kam.KeyframeAnimationInfo[] = ruleset[animationsSymbol];
|
||||
if (animations !== undefined && animations.length) {
|
||||
ensureCssAnimationParserModule();
|
||||
for (let animation of animations) {
|
||||
const cssKeyframe = this._keyframes[animation.name];
|
||||
if (cssKeyframe !== undefined) {
|
||||
animation.keyframes = cssAnimationParserModule
|
||||
.CssAnimationParser.keyframesArrayFromCSS(cssKeyframe.keyframes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getAnimations(ruleset: RuleSet): kam.KeyframeAnimationInfo[] {
|
||||
return ruleset[animationsSymbol];
|
||||
}
|
||||
}
|
||||
|
||||
type KeyframesMap = Map<string, Keyframes>;
|
||||
|
||||
export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (name: string) => boolean, importSource?: string): string {
|
||||
|
||||
let fileName: string = typeof url === "string" ? url.trim() : "";
|
||||
if (fileName.indexOf("~/") === 0) {
|
||||
fileName = fileName.replace("~/", "");
|
||||
}
|
||||
|
||||
const isAbsolutePath = fileName.indexOf("/") === 0;
|
||||
const absolutePath = isAbsolutePath ? fileName : path.join(appDirectory, fileName);
|
||||
if (fileExists(absolutePath)) {
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
if (!isAbsolutePath) {
|
||||
if (fileName[0] === "~" && fileName[1] !== "/" && fileName[1] !== "\"") {
|
||||
fileName = fileName.substr(1);
|
||||
}
|
||||
|
||||
if (importSource) {
|
||||
const importFile = resolveFilePathFromImport(importSource, fileName);
|
||||
if (fileExists(importFile)) {
|
||||
return importFile;
|
||||
}
|
||||
}
|
||||
|
||||
const external = path.join(appDirectory, "tns_modules", fileName);
|
||||
if (fileExists(external)) {
|
||||
return external;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveFilePathFromImport(importSource: string, fileName: string): string {
|
||||
const importSourceParts = importSource.split(path.separator);
|
||||
const fileNameParts = fileName.split(path.separator)
|
||||
// exclude the dot-segment for current directory
|
||||
.filter(p => !isCurrentDirectory(p));
|
||||
|
||||
// remove current file name
|
||||
importSourceParts.pop();
|
||||
// remove element in case of dot-segment for parent directory or add file name
|
||||
fileNameParts.forEach(p => isParentDirectory(p) ? importSourceParts.pop() : importSourceParts.push(p));
|
||||
|
||||
return importSourceParts.join(path.separator);
|
||||
}
|
||||
|
||||
export const applyInlineStyle = profile(function applyInlineStyle(view: ViewBase, styleStr: string) {
|
||||
let localStyle = `local { ${styleStr} }`;
|
||||
let inlineRuleSet = CSSSource.fromSource(localStyle, new Map()).selectors;
|
||||
|
||||
// Reset unscoped css-variables
|
||||
view.style.resetUnscopedCssVariables();
|
||||
|
||||
// Set all the css-variables first, so we can be sure they are up-to-date
|
||||
inlineRuleSet[0].declarations.forEach(d => {
|
||||
// Use the actual property name so that a local value is set.
|
||||
let property = d.property;
|
||||
if (isCssVariable(property)) {
|
||||
view.style.setUnscopedCssVariable(property, d.value);
|
||||
}
|
||||
});
|
||||
|
||||
inlineRuleSet[0].declarations.forEach(d => {
|
||||
// Use the actual property name so that a local value is set.
|
||||
let property = d.property;
|
||||
try {
|
||||
if (isCssVariable(property)) {
|
||||
// Skip css-variables, they have been handled
|
||||
return;
|
||||
}
|
||||
|
||||
const value = evaluateCssExpressions(view, property, d.value);
|
||||
if (property in view.style) {
|
||||
view.style[property] = value;
|
||||
} else {
|
||||
view[property] = value;
|
||||
}
|
||||
} catch (e) {
|
||||
traceWrite(`Failed to apply property [${d.property}] with value [${d.value}] to ${view}. ${e}`, traceCategories.Error, traceMessageType.error);
|
||||
}
|
||||
});
|
||||
|
||||
// This is needed in case of changes to css-variable or css-calc expressions.
|
||||
view._onCssStateChange();
|
||||
});
|
||||
|
||||
function isCurrentDirectory(uriPart: string): boolean {
|
||||
return uriPart === ".";
|
||||
}
|
||||
|
||||
function isParentDirectory(uriPart: string): boolean {
|
||||
return uriPart === "..";
|
||||
}
|
||||
|
||||
function isKeyframe(node: CssNode): node is KeyframesDefinition {
|
||||
return node.type === "keyframes";
|
||||
}
|
||||
5
nativescript-core/ui/styling/style/package.json
Normal file
5
nativescript-core/ui/styling/style/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "style",
|
||||
"main": "style",
|
||||
"types": "style.d.ts"
|
||||
}
|
||||
234
nativescript-core/ui/styling/style/style.d.ts
vendored
Normal file
234
nativescript-core/ui/styling/style/style.d.ts
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* @module "ui/styling/style"
|
||||
*/ /** */
|
||||
|
||||
import { Length, PercentLength, ViewBase, Observable, BackgroundRepeat, Visibility, HorizontalAlignment, VerticalAlignment, dip } from "../../core/view";
|
||||
import { Color } from "../../../color";
|
||||
import { Background } from "../background";
|
||||
import { Font, FontStyle, FontWeight } from "../font";
|
||||
import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from "../../text-base";
|
||||
import {
|
||||
FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent,
|
||||
Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf
|
||||
} from "../../layouts/flexbox-layout";
|
||||
import { LinearGradient } from "../gradient";
|
||||
|
||||
export interface Thickness {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export interface BorderColor {
|
||||
top: Color;
|
||||
right: Color;
|
||||
bottom: Color;
|
||||
left: Color;
|
||||
}
|
||||
|
||||
export interface CommonLayoutParams {
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
widthPercent: number;
|
||||
heightPercent: number;
|
||||
|
||||
leftMargin: number;
|
||||
topMargin: number;
|
||||
rightMargin: number;
|
||||
bottomMargin: number;
|
||||
|
||||
leftMarginPercent: number;
|
||||
topMarginPercent: number;
|
||||
rightMarginPercent: number;
|
||||
bottomMarginPercent: number;
|
||||
|
||||
horizontalAlignment: HorizontalAlignment;
|
||||
verticalAlignment: VerticalAlignment;
|
||||
}
|
||||
|
||||
export class Style extends Observable {
|
||||
public fontInternal: Font;
|
||||
public backgroundInternal: Background;
|
||||
|
||||
public rotate: number;
|
||||
public rotateX: number;
|
||||
public rotateY: number;
|
||||
public perspective: number;
|
||||
public scaleX: number;
|
||||
public scaleY: number;
|
||||
public translateX: dip;
|
||||
public translateY: dip;
|
||||
|
||||
public clipPath: string;
|
||||
public color: Color;
|
||||
public tintColor: Color;
|
||||
public placeholderColor: Color;
|
||||
|
||||
public background: string;
|
||||
public backgroundColor: Color;
|
||||
public backgroundImage: string | LinearGradient;
|
||||
public backgroundRepeat: BackgroundRepeat;
|
||||
public backgroundSize: string;
|
||||
public backgroundPosition: string;
|
||||
|
||||
public borderColor: string | Color;
|
||||
public borderTopColor: Color;
|
||||
public borderRightColor: Color;
|
||||
public borderBottomColor: Color;
|
||||
public borderLeftColor: Color;
|
||||
public borderWidth: string | Length;
|
||||
public borderTopWidth: Length;
|
||||
public borderRightWidth: Length;
|
||||
public borderBottomWidth: Length;
|
||||
public borderLeftWidth: Length;
|
||||
public borderRadius: string | Length;
|
||||
public borderTopLeftRadius: Length;
|
||||
public borderTopRightRadius: Length;
|
||||
public borderBottomRightRadius: Length;
|
||||
public borderBottomLeftRadius: Length;
|
||||
|
||||
public fontSize: number;
|
||||
public fontFamily: string;
|
||||
public fontStyle: FontStyle;
|
||||
public fontWeight: FontWeight;
|
||||
public font: string;
|
||||
|
||||
public androidElevation: number;
|
||||
public androidDynamicElevationOffset: number;
|
||||
public zIndex: number;
|
||||
public opacity: number;
|
||||
public visibility: Visibility;
|
||||
|
||||
public letterSpacing: number;
|
||||
public lineHeight: number;
|
||||
public textAlignment: TextAlignment;
|
||||
public textDecoration: TextDecoration;
|
||||
public textTransform: TextTransform;
|
||||
public whiteSpace: WhiteSpace;
|
||||
|
||||
public minWidth: Length;
|
||||
public minHeight: Length;
|
||||
public width: PercentLength;
|
||||
public height: PercentLength;
|
||||
public margin: string | PercentLength;
|
||||
public marginLeft: PercentLength;
|
||||
public marginTop: PercentLength;
|
||||
public marginRight: PercentLength;
|
||||
public marginBottom: PercentLength;
|
||||
public padding: string | Length;
|
||||
public paddingLeft: Length;
|
||||
public paddingTop: Length;
|
||||
public paddingRight: Length;
|
||||
public paddingBottom: Length;
|
||||
public horizontalAlignment: HorizontalAlignment;
|
||||
public verticalAlignment: VerticalAlignment;
|
||||
|
||||
// TabView-specific props
|
||||
public tabTextFontSize: number;
|
||||
public tabTextColor: Color;
|
||||
public tabBackgroundColor: Color;
|
||||
public selectedTabTextColor: Color;
|
||||
public androidSelectedTabHighlightColor: Color;
|
||||
|
||||
// ListView-specific props
|
||||
public separatorColor: Color;
|
||||
|
||||
// SegmentedBar-specific props
|
||||
public selectedBackgroundColor: Color;
|
||||
|
||||
// Page-specific props
|
||||
public statusBarStyle: "light" | "dark";
|
||||
public androidStatusBarBackground: Color;
|
||||
|
||||
// Android ActionBar specific props
|
||||
|
||||
/**
|
||||
* Gets or sets the content inset for the android actionbar.
|
||||
* The content inset affects the valid area for ActionBar content; insets can be used to effectively align ActionBar content along well-known gridlines.
|
||||
*
|
||||
* This property is effective on Android API level 21 or later.
|
||||
*/
|
||||
public androidContentInset: string | Length;
|
||||
|
||||
/**
|
||||
* Gets or sets the left content inset for the android actionbar.
|
||||
* The content inset affects the valid area for ActionBar content; insets can be used to effectively align ActionBar content along well-known gridlines.
|
||||
*
|
||||
* This property is effective on Android API level 21 or later.
|
||||
*/
|
||||
public androidContentInsetLeft: Length;
|
||||
|
||||
/**
|
||||
* Gets or sets the right content inset for the android actionbar.
|
||||
* The content inset affects the valid area for ActionBar content; insets can be used to effectively align ActionBar content along well-known gridlines.
|
||||
*
|
||||
* This property is effective on Android API level 21 or later.
|
||||
*/
|
||||
public androidContentInsetRight: Length;
|
||||
|
||||
constructor(ownerView: ViewBase | WeakRef<ViewBase>);
|
||||
public viewRef: WeakRef<ViewBase>;
|
||||
|
||||
/**
|
||||
* @deprecated use `viewRef` instead.
|
||||
*
|
||||
* The `ViewBase` object associated with the Style!
|
||||
*/
|
||||
public view: ViewBase;
|
||||
|
||||
//flexbox layout properties
|
||||
public flexDirection: FlexDirection;
|
||||
public flexWrap: FlexWrap;
|
||||
public justifyContent: JustifyContent;
|
||||
public alignItems: AlignItems;
|
||||
public alignContent: AlignContent;
|
||||
public order: Order;
|
||||
public flexGrow: FlexGrow;
|
||||
public flexShrink: FlexShrink;
|
||||
public flexWrapBefore: FlexWrapBefore;
|
||||
public alignSelf: AlignSelf;
|
||||
|
||||
/**
|
||||
* The property bag is a simple class that is paired with the Style class.
|
||||
* Setting regular css properties on the PropertyBag should simply preserve their values.
|
||||
* Setting shorthand css properties on the PropertyBag should decompose the provided value, and set each of the shorthand composite properties.
|
||||
* The shorthand properties are defined as non-enumerable so it should be safe to for-in the keys that are set in the bag.
|
||||
*/
|
||||
public readonly PropertyBag: PropertyBagClass;
|
||||
|
||||
/**
|
||||
* Set a scoped css-value. These are css-variables set from CssState
|
||||
*/
|
||||
public setScopedCssVariable(varName: string, value: string): void;
|
||||
|
||||
/**
|
||||
* Set a unscoped css-value. These are css-variables set on view.style
|
||||
*/
|
||||
public setUnscopedCssVariable(varName: string, value: string): void;
|
||||
|
||||
/**
|
||||
* Get value of the css-variable.
|
||||
* If the value is not set on this style-object, try the parent view.
|
||||
*/
|
||||
public getCssVariable(varName: string): string | null;
|
||||
|
||||
/**
|
||||
* Remove all scoped css-variables
|
||||
*/
|
||||
public resetScopedCssVariables(): void;
|
||||
|
||||
/**
|
||||
* Remove all unscoped css-variables
|
||||
*/
|
||||
public resetUnscopedCssVariables(): void;
|
||||
}
|
||||
|
||||
interface PropertyBagClass {
|
||||
new(): PropertyBag;
|
||||
prototype: PropertyBag;
|
||||
}
|
||||
interface PropertyBag {
|
||||
[property: string]: string;
|
||||
}
|
||||
208
nativescript-core/ui/styling/style/style.ts
Normal file
208
nativescript-core/ui/styling/style/style.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { Style as StyleDefinition } from ".";
|
||||
import { Color } from "../../../color";
|
||||
import { Font, FontStyle, FontWeight } from "../font";
|
||||
import { Background } from "../background";
|
||||
import {
|
||||
Length, PercentLength, ViewBase, BackgroundRepeat, Visibility,
|
||||
HorizontalAlignment, VerticalAlignment, dip, LinearGradient
|
||||
} from "../../core/view";
|
||||
import { Observable } from "../../../data/observable";
|
||||
|
||||
import {
|
||||
FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent,
|
||||
Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf
|
||||
} from "../../layouts/flexbox-layout";
|
||||
import {
|
||||
write as traceWrite,
|
||||
categories as traceCategories,
|
||||
messageType as traceMessageType,
|
||||
} from "../../../trace";
|
||||
import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from "../../text-base";
|
||||
|
||||
export class Style extends Observable implements StyleDefinition {
|
||||
private unscopedCssVariables = new Map<string, string>();
|
||||
private scopedCssVariables = new Map<string, string>();
|
||||
|
||||
constructor(ownerView: ViewBase | WeakRef<ViewBase>) {
|
||||
super();
|
||||
|
||||
// HACK: Could not find better way for cross platform WeakRef type checking.
|
||||
if (ownerView.constructor.toString().indexOf("[native code]") !== -1) {
|
||||
this.viewRef = <WeakRef<ViewBase>>ownerView;
|
||||
} else {
|
||||
this.viewRef = new WeakRef(<ViewBase>ownerView);
|
||||
}
|
||||
}
|
||||
|
||||
public setScopedCssVariable(varName: string, value: string): void {
|
||||
this.scopedCssVariables.set(varName, value);
|
||||
}
|
||||
|
||||
public setUnscopedCssVariable(varName: string, value: string): void {
|
||||
this.unscopedCssVariables.set(varName, value);
|
||||
}
|
||||
|
||||
public getCssVariable(varName: string): string | null {
|
||||
const view = this.view;
|
||||
if (!view) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.unscopedCssVariables.has(varName)) {
|
||||
return this.unscopedCssVariables.get(varName);
|
||||
}
|
||||
|
||||
if (this.scopedCssVariables.has(varName)) {
|
||||
return this.scopedCssVariables.get(varName);
|
||||
}
|
||||
|
||||
if (!view.parent || !view.parent.style) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return view.parent.style.getCssVariable(varName);
|
||||
}
|
||||
|
||||
public resetScopedCssVariables() {
|
||||
this.scopedCssVariables.clear();
|
||||
}
|
||||
|
||||
public resetUnscopedCssVariables() {
|
||||
this.unscopedCssVariables.clear();
|
||||
}
|
||||
|
||||
toString() {
|
||||
const view = this.viewRef.get();
|
||||
if (!view) {
|
||||
traceWrite(`toString() of Style cannot execute correctly because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${view}.style`;
|
||||
}
|
||||
|
||||
public fontInternal: Font;
|
||||
public backgroundInternal: Background;
|
||||
|
||||
public rotate: number;
|
||||
public rotateX: number;
|
||||
public rotateY: number;
|
||||
public perspective: number;
|
||||
|
||||
public scaleX: number;
|
||||
public scaleY: number;
|
||||
public translateX: dip;
|
||||
public translateY: dip;
|
||||
|
||||
public clipPath: string;
|
||||
public color: Color;
|
||||
public tintColor: Color;
|
||||
public placeholderColor: Color;
|
||||
|
||||
public background: string;
|
||||
public backgroundColor: Color;
|
||||
public backgroundImage: string | LinearGradient;
|
||||
public backgroundRepeat: BackgroundRepeat;
|
||||
public backgroundSize: string;
|
||||
public backgroundPosition: string;
|
||||
|
||||
public borderColor: string | Color;
|
||||
public borderTopColor: Color;
|
||||
public borderRightColor: Color;
|
||||
public borderBottomColor: Color;
|
||||
public borderLeftColor: Color;
|
||||
public borderWidth: string | Length;
|
||||
public borderTopWidth: Length;
|
||||
public borderRightWidth: Length;
|
||||
public borderBottomWidth: Length;
|
||||
public borderLeftWidth: Length;
|
||||
public borderRadius: string | Length;
|
||||
public borderTopLeftRadius: Length;
|
||||
public borderTopRightRadius: Length;
|
||||
public borderBottomRightRadius: Length;
|
||||
public borderBottomLeftRadius: Length;
|
||||
|
||||
public fontSize: number;
|
||||
public fontFamily: string;
|
||||
public fontStyle: FontStyle;
|
||||
public fontWeight: FontWeight;
|
||||
public font: string;
|
||||
|
||||
public androidElevation: number;
|
||||
public androidDynamicElevationOffset: number;
|
||||
public zIndex: number;
|
||||
public opacity: number;
|
||||
public visibility: Visibility;
|
||||
|
||||
public letterSpacing: number;
|
||||
public lineHeight: number;
|
||||
public textAlignment: TextAlignment;
|
||||
public textDecoration: TextDecoration;
|
||||
public textTransform: TextTransform;
|
||||
public whiteSpace: WhiteSpace;
|
||||
|
||||
public minWidth: Length;
|
||||
public minHeight: Length;
|
||||
public width: PercentLength;
|
||||
public height: PercentLength;
|
||||
public margin: string | PercentLength;
|
||||
public marginLeft: PercentLength;
|
||||
public marginTop: PercentLength;
|
||||
public marginRight: PercentLength;
|
||||
public marginBottom: PercentLength;
|
||||
public padding: string | Length;
|
||||
public paddingLeft: Length;
|
||||
public paddingTop: Length;
|
||||
public paddingRight: Length;
|
||||
public paddingBottom: Length;
|
||||
public horizontalAlignment: HorizontalAlignment;
|
||||
public verticalAlignment: VerticalAlignment;
|
||||
|
||||
// TabView-specific props
|
||||
public tabTextFontSize: number;
|
||||
public tabTextColor: Color;
|
||||
public tabBackgroundColor: Color;
|
||||
public selectedTabTextColor: Color;
|
||||
public androidSelectedTabHighlightColor: Color;
|
||||
|
||||
// ListView-specific props
|
||||
public separatorColor: Color;
|
||||
|
||||
//SegmentedBar-specific props
|
||||
public selectedBackgroundColor: Color;
|
||||
|
||||
// Page-specific props
|
||||
public statusBarStyle: "light" | "dark";
|
||||
public androidStatusBarBackground: Color;
|
||||
|
||||
// ActionBar-specific props
|
||||
public androidContentInset: string | Length;
|
||||
public androidContentInsetLeft: Length;
|
||||
public androidContentInsetRight: Length;
|
||||
|
||||
//flexbox layout properties
|
||||
public flexDirection: FlexDirection;
|
||||
public flexWrap: FlexWrap;
|
||||
public justifyContent: JustifyContent;
|
||||
public alignItems: AlignItems;
|
||||
public alignContent: AlignContent;
|
||||
public order: Order;
|
||||
public flexGrow: FlexGrow;
|
||||
public flexShrink: FlexShrink;
|
||||
public flexWrapBefore: FlexWrapBefore;
|
||||
public alignSelf: AlignSelf;
|
||||
|
||||
public PropertyBag: { new(): { [property: string]: string }, prototype: { [property: string]: string } };
|
||||
|
||||
public viewRef: WeakRef<ViewBase>;
|
||||
|
||||
public get view(): ViewBase {
|
||||
if (this.viewRef) {
|
||||
return this.viewRef.get();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Style.prototype.PropertyBag = class { [property: string]: string; };
|
||||
Reference in New Issue
Block a user