← 输入框|github.com|刷新
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core/core-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,13 @@ export namespace CoreTypes {
export const parse = makeParser<CornerShapeType>(isValid);
}

export type IOSCornerShapeType = CornerShapeType | 'continuous';
export namespace IOSCornerShape {
export const continuous: IOSCornerShapeType = 'continuous';
export const isValid = makeValidator<IOSCornerShapeType>(CornerShape.round, CornerShape.squircle, continuous);
export const parse = makeParser<IOSCornerShapeType>(isValid);
}

export namespace AnimationCurve {
export const ease = 'ease';
export const easeIn = 'easeIn';
Expand Down
4 changes: 2 additions & 2 deletions packages/core/ui/styling/background-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class Background {
public borderTopRightRadius = 0;
public borderBottomLeftRadius = 0;
public borderBottomRightRadius = 0;
public cornerShape: CoreTypes.CornerShapeType = CoreTypes.CornerShape.round;
public cornerShape: CoreTypes.IOSCornerShapeType = CoreTypes.CornerShape.round;
public clipPath: string | ClipPathFunction;
public boxShadows: BoxShadow[];
public clearFlags: number = BackgroundClearFlags.NONE;
Expand Down Expand Up @@ -194,7 +194,7 @@ export class Background {
return clone;
}

public withCornerShape(value: CoreTypes.CornerShapeType): Background {
public withCornerShape(value: CoreTypes.IOSCornerShapeType): Background {
const clone = this.clone();
clone.cornerShape = value;

Expand Down
7 changes: 7 additions & 0 deletions packages/core/ui/styling/background.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ export namespace ios {
export function createUIImageFromURI(view: View, imageURI: string, flip: boolean, callback: (image: any) => void): void;
export function generateClipPath(view: View, bounds: any /* CGRect */): any;
export function generateShadowLayerPaths(view: View, bounds: any /* CGRect */): { maskPath: any; shadowPath: any };
/**
* Whether the layer's own cornerRadius and cornerCurve can draw the corners.
* The CSS squircle has no CALayer curve, so it always goes through a path mask.
* @param background The view's background.
* @returns True when no path mask is needed for the corners.
*/
export function drawsCornersNatively(background: BackgroundDefinition): boolean;
export function getUniformBorderRadius(view: View, bounds: any /* CGRect */): number;
export function generateNonUniformBorderInnerClipRoundedPath(view: View, bounds: any /* CGRect */): any;
export function generateNonUniformBorderOuterClipRoundedPath(view: View, bounds: any /* CGRect */): any;
Expand Down
22 changes: 16 additions & 6 deletions packages/core/ui/styling/background.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ export namespace ios {
// Initialize clipping mask (usually for clip-path and non-uniform rounded borders)
maskLayerIfNeeded(nativeView, background);

if (background.hasUniformBorder()) {
if (drawsCornersNatively(background)) {
const borderColor = background.getUniformBorderColor();
layer.borderColor = borderColor?.ios?.CGColor;
layer.borderWidth = layout.toDeviceIndependentPixels(background.getUniformBorderWidth());
layer.cornerRadius = getUniformBorderRadius(view, layer.bounds);
layer.cornerCurve = background.cornerShape === CoreTypes.CornerShape.squircle ? kCACornerCurveContinuous : kCACornerCurveCircular;
layer.cornerCurve = background.cornerShape === CoreTypes.IOSCornerShape.continuous ? kCACornerCurveContinuous : kCACornerCurveCircular;
} else {
drawNonUniformBorders(nativeView, background);
needsLayerAdjustmentOnScroll = true;
Expand Down Expand Up @@ -128,7 +128,7 @@ export namespace ios {
let needsMask;
switch (nativeView.maskType) {
case iosViewUtils.LayerMask.BORDER:
needsMask = !background.hasUniformBorder() && background.hasBorderRadius();
needsMask = !drawsCornersNatively(background) && background.hasBorderRadius();
break;
case iosViewUtils.LayerMask.CLIP_PATH:
needsMask = !!background.clipPath;
Expand All @@ -153,7 +153,7 @@ export namespace ios {
clearNonUniformColorBorders(nativeView);
}

if (background.hasUniformBorder()) {
if (drawsCornersNatively(background)) {
clearNonUniformBorders(nativeView);
}
}
Expand Down Expand Up @@ -232,7 +232,7 @@ export namespace ios {

// Generate more detailed paths if view has border radius
if (background.hasBorderRadius()) {
if (background.hasUniformBorder()) {
if (drawsCornersNatively(background)) {
const cornerRadius = layer.cornerRadius;
const cappedRadius = getBorderCapRadius(cornerRadius, width / 2, height / 2);
const cappedOuterRadii: CappedOuterRadii = {
Expand Down Expand Up @@ -333,6 +333,16 @@ export namespace ios {
return path;
}

/**
* Whether the layer's own cornerRadius and cornerCurve can draw the corners.
* The CSS squircle has no CALayer curve, so it always goes through a path mask.
* @param background The view's background.
* @returns True when no path mask is needed for the corners.
*/
export function drawsCornersNatively(background: BackgroundDefinition): boolean {
return background.hasUniformBorder() && background.cornerShape !== CoreTypes.CornerShape.squircle;
}

export function getUniformBorderRadius(view: View, bounds: CGRect): number {
const background = view.style.backgroundInternal;
const { width, height } = bounds.size;
Expand Down Expand Up @@ -370,7 +380,7 @@ function maskLayerIfNeeded(nativeView: NativeScriptUIView, background: Backgroun
// Since layers can only accept up to a single mask at a time, clip path is given more priority
if (background.clipPath) {
nativeView.maskType = iosViewUtils.LayerMask.CLIP_PATH;
} else if (!background.hasUniformBorder() && background.hasBorderRadius()) {
} else if (!ios.drawsCornersNatively(background) && background.hasBorderRadius()) {
nativeView.maskType = iosViewUtils.LayerMask.BORDER;
} else {
nativeView.maskType = null;
Expand Down
68 changes: 67 additions & 1 deletion packages/core/ui/styling/style-properties.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';

import { Label } from '../label';
import { Color } from '../../color';

function onAndroid(): void {
let previousApple: boolean;
let previousIOS: boolean;
let previousAndroid: boolean;

beforeEach(() => {
previousApple = (globalThis as any).__APPLE__;
previousIOS = (globalThis as any).__IOS__;
previousAndroid = (globalThis as any).__ANDROID__;
(globalThis as any).__APPLE__ = false;
(globalThis as any).__IOS__ = false;
(globalThis as any).__ANDROID__ = true;
});

afterEach(() => {
(globalThis as any).__APPLE__ = previousApple;
(globalThis as any).__IOS__ = previousIOS;
(globalThis as any).__ANDROID__ = previousAndroid;
});
}

describe('border-color shorthand', () => {
it('applies a function color with nested arguments to all sides', () => {
const label = new Label();
Expand Down Expand Up @@ -60,4 +81,49 @@ describe('corner-shape', () => {

expect(() => (label.style.cornerShape = 'bevel' as any)).toThrow();
});

it('accepts continuous on Apple platforms', () => {
const label = new Label();
label.style.cornerShape = 'continuous';

expect(label.style.backgroundInternal.cornerShape).toBe('continuous');
});
});

describe('-ios-corner-shape', () => {
it('sets corner-shape and reads it back', () => {
const label = new Label();
label.style.iosCornerShape = 'continuous';

expect(label.style.cornerShape).toBe('continuous');
expect(label.style.iosCornerShape).toBe('continuous');
expect(label.style.backgroundInternal.cornerShape).toBe('continuous');
});

it('rejects unknown keywords', () => {
const label = new Label();

expect(() => (label.style.iosCornerShape = 'bevel' as any)).toThrow();
});
});

describe('corner-shape on Android', () => {
onAndroid();

it('keeps the spec grammar', () => {
const label = new Label();

expect(() => (label.style.cornerShape = 'continuous')).toThrow();
label.style.cornerShape = 'squircle';
expect(label.style.backgroundInternal.cornerShape).toBe('squircle');
});

it('ignores -ios-corner-shape', () => {
const label = new Label();
label.style.iosCornerShape = 'continuous';

expect(label.style.cornerShape).toBe('round');
expect(label.style.iosCornerShape).toBe('round');
expect(label.style.backgroundInternal.cornerShape).toBe('round');
});
});
22 changes: 20 additions & 2 deletions packages/core/ui/styling/style-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1053,17 +1053,35 @@ export const borderBottomLeftRadiusProperty = new CssProperty<Style, CoreTypes.L
});
borderBottomLeftRadiusProperty.register(Style);

export const cornerShapeProperty = new CssProperty<Style, CoreTypes.CornerShapeType>({
export const cornerShapeProperty = new CssProperty<Style, CoreTypes.IOSCornerShapeType>({
name: 'cornerShape',
cssName: 'corner-shape',
defaultValue: CoreTypes.CornerShape.round,
valueConverter: CoreTypes.CornerShape.parse,
// Only CALayer can draw `continuous`; other platforms keep the spec grammar.
valueConverter: (value) => (__APPLE__ ? CoreTypes.IOSCornerShape.parse(value) : CoreTypes.CornerShape.parse(value)),
valueChanged: (target, oldValue, newValue) => {
target.backgroundInternal = target.backgroundInternal.withCornerShape(newValue);
},
});
cornerShapeProperty.register(Style);

export const iosCornerShapeProperty = new ShorthandProperty<Style, CoreTypes.IOSCornerShapeType>({
name: 'iosCornerShape',
cssName: '-ios-corner-shape',
getter: function (this: Style) {
return this.cornerShape;
},
// Alias of corner-shape: cascades in its place on Apple, dropped elsewhere.
converter: function (value) {
if (!__APPLE__) {
return [];
}

return [[cornerShapeProperty, typeof value === 'string' ? CoreTypes.IOSCornerShape.parse(value) : value]];
},
});
iosCornerShapeProperty.register(Style);

const boxShadowProperty = new CssProperty<Style, ShadowCSSValues[]>({
name: 'boxShadow',
cssName: 'box-shadow',
Expand Down
90 changes: 89 additions & 1 deletion packages/core/ui/styling/style-scope.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
import { describe, it, expect, beforeAll } from 'vitest';
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';

import { StyleScope, applyInlineStyle, addTaggedAdditionalCSS, removeTaggedAdditionalCSS } from './style-scope';
import { StackLayout } from '../layouts/stack-layout';
import { Label } from '../label';

function onAndroid(): void {
let previousApple: boolean;
let previousIOS: boolean;
let previousAndroid: boolean;

beforeEach(() => {
previousApple = (globalThis as any).__APPLE__;
previousIOS = (globalThis as any).__IOS__;
previousAndroid = (globalThis as any).__ANDROID__;
(globalThis as any).__APPLE__ = false;
(globalThis as any).__IOS__ = false;
(globalThis as any).__ANDROID__ = true;
});

afterEach(() => {
(globalThis as any).__APPLE__ = previousApple;
(globalThis as any).__IOS__ = previousIOS;
(globalThis as any).__ANDROID__ = previousAndroid;
});
}

/**
* Counts how many times css applies a value to a style property, by wrapping the
* `css:<name>` accessor the css state writes through.
Expand Down Expand Up @@ -43,6 +64,73 @@ function styled(css: string, className?: string): { view: any; scope: StyleScope
return { view, scope };
}

describe('-ios-corner-shape alias', () => {
it('cascades in place of corner-shape, so the later declaration wins', () => {
const { view } = styled('label { corner-shape: squircle; -ios-corner-shape: continuous; }');
view._cssState.onLoaded();

expect(view.style.cornerShape).toBe('continuous');
});

it('loses to a later corner-shape declaration', () => {
const { view } = styled('label { -ios-corner-shape: continuous; corner-shape: squircle; }');
view._cssState.onLoaded();

expect(view.style.cornerShape).toBe('squircle');
});

it('follows specificity like any other declaration', () => {
const { view } = styled('label.card { corner-shape: squircle; } label { -ios-corner-shape: continuous; }');
view.className = 'card';
view._cssState.onLoaded();

expect(view.style.cornerShape).toBe('squircle');
});

it('is unset when its rule stops matching', () => {
const { view } = styled('label:highlighted { -ios-corner-shape: continuous; }');
view._cssState.onLoaded();

view.addPseudoClass('highlighted');
expect(view.style.cornerShape).toBe('continuous');

view.deletePseudoClass('highlighted');
expect(view.style.cornerShape).toBe('round');
});

it('applies from an inline style', () => {
const view = new Label();
applyInlineStyle(view, '-ios-corner-shape: continuous');

expect(view.style.cornerShape).toBe('continuous');
});
});

describe('-ios-corner-shape alias on Android', () => {
onAndroid();

it('is dropped from the cascade', () => {
const { view } = styled('label { corner-shape: squircle; -ios-corner-shape: continuous; }');
view._cssState.onLoaded();

expect(view.style.cornerShape).toBe('squircle');
});

it('leaves corner-shape at its default when declared alone', () => {
const { view } = styled('label { -ios-corner-shape: continuous; }');
view._cssState.onLoaded();

expect(view.style.cornerShape).toBe('round');
});

it('is ignored in an inline style', () => {
const view = new Label();
applyInlineStyle(view, '-ios-corner-shape: continuous');

expect(view.style.cornerShape).toBe('round');
});
});

describe('CssState.setPropertyValues', () => {
it('applies matched declarations', () => {
const { view } = styled('label { color: red; }');
Expand Down
15 changes: 12 additions & 3 deletions packages/core/ui/styling/style/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,19 @@ export class Style extends Observable {

/**
* The curve used to round the corners: 'round' (circular arcs, the default)
* or 'squircle' (Apple's continuous corner curve). iOS only; Android always
* renders circular corners.
* or 'squircle' (the CSS superellipse). On Apple platforms it also accepts
* 'continuous' (Apple's continuous corner curve), normally set through
* iosCornerShape. Android always renders circular corners.
*/
public cornerShape: CoreTypes.CornerShapeType;
public cornerShape: CoreTypes.IOSCornerShapeType;

/**
* Apple-only alias of cornerShape (`-ios-corner-shape` in CSS). It cascades
* in place of cornerShape on Apple platforms and is ignored elsewhere, so
* `corner-shape: squircle; -ios-corner-shape: continuous;` renders the
* continuous curve on iOS and the squircle on Android.
*/
public iosCornerShape: CoreTypes.IOSCornerShapeType;

public boxShadow: string | ShadowCSSValues[];

Expand Down
Loading