From 539fd1eb293241dc067bfdcc77613b0eb67b099f Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Mon, 16 Nov 2020 06:57:58 +0100 Subject: [PATCH 1/5] fix(core): notify object now optional (#9032) --- packages/core/data/observable/index.d.ts | 9 ++++++++- packages/core/data/observable/index.ts | 15 +++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/core/data/observable/index.d.ts b/packages/core/data/observable/index.d.ts index 77c0f19ed..4cb017139 100644 --- a/packages/core/data/observable/index.d.ts +++ b/packages/core/data/observable/index.d.ts @@ -12,6 +12,13 @@ export interface EventData { object: Observable; } + +export interface NotifyData extends Partial { + eventName: string; + object?: Observable; +} + + /** * Data for the "propertyChange" event. */ @@ -136,7 +143,7 @@ export class Observable { * Notifies all the registered listeners for the event provided in the data.eventName. * @param data The data associated with the event. */ - notify(data: T): void; + notify(data: T): void; /** * Notifies all the registered listeners for the property change event. diff --git a/packages/core/data/observable/index.ts b/packages/core/data/observable/index.ts index 4b198d7df..64e8026ce 100644 --- a/packages/core/data/observable/index.ts +++ b/packages/core/data/observable/index.ts @@ -5,6 +5,11 @@ export interface EventData { object: Observable; } +export interface NotifyData extends Partial { + eventName: string; + object?: Observable; +} + export interface PropertyChangeData extends EventData { propertyName: string; value: any; @@ -262,16 +267,18 @@ export class Observable implements ObservableDefinition { } } - public notify(data: T): void { + public notify(data: T): void { + const eventData = data as EventData; + eventData.object = eventData.object || this; const eventClass = this.constructor.name; - this._globalNotify(eventClass, 'First', data); + this._globalNotify(eventClass, 'First', eventData); const observers = >this._observers[data.eventName]; if (observers) { - Observable._handleEvent(observers, data); + Observable._handleEvent(observers, eventData); } - this._globalNotify(eventClass, '', data); + this._globalNotify(eventClass, '', eventData); } private static _handleEvent(observers: Array, data: T): void { From 1769de903392ab14ffb6c366ab86dc24b5289e81 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Thu, 19 Nov 2020 18:07:51 -0300 Subject: [PATCH 2/5] feat(android): setInterval closer to web spec (#9044) --- apps/automated/src/timer/timer-tests.ts | 34 +++++++++++++++++++++++++ packages/core/timer/index.android.ts | 8 ++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/apps/automated/src/timer/timer-tests.ts b/apps/automated/src/timer/timer-tests.ts index 99c633828..4f8f10bbb 100644 --- a/apps/automated/src/timer/timer-tests.ts +++ b/apps/automated/src/timer/timer-tests.ts @@ -171,6 +171,40 @@ export function test_setInterval_callbackCalledWithExtraArgs(done) { ); } +export function test_setInterval_callbackNotDelayedByBusyWork() { + let calls = 0; + + let firstCall = true; + const id = timer.setInterval(() => { + calls++; + if (firstCall) { + firstCall = false; + TKUnit.wait(0.025); + } + }, 50); + + TKUnit.wait(0.11); + timer.clearInterval(id); + TKUnit.assertEqual(calls, 2, 'Callback should be called multiple times with busy wait'); +} + +export function test_setInterval_callbackSkippedByBusyWork() { + let calls = 0; + + let firstCall = true; + const id = timer.setInterval(() => { + calls++; + if (firstCall) { + firstCall = false; + TKUnit.wait(0.051); + } + }, 50); + + TKUnit.wait(0.16); + timer.clearInterval(id); + TKUnit.assertEqual(calls, 2, 'Callback should be called skipped when it takes too long to process'); +} + export function test_setInterval_callbackShouldBeCleared(done) { const start = TKUnit.time(); // >> timer-set-interval diff --git a/packages/core/timer/index.android.ts b/packages/core/timer/index.android.ts index 06d42d958..8bf793113 100644 --- a/packages/core/timer/index.android.ts +++ b/packages/core/timer/index.android.ts @@ -58,12 +58,16 @@ export function setInterval(callback: Function, milliseconds = 0, ...args): numb const handler = timeoutHandler; const invoke = () => callback(...args); const zoneBound = zonedCallback(invoke); + const startOffset = milliseconds > 0 ? Date.now() % milliseconds : 0; + function nextCallMs() { + return milliseconds > 0 ? milliseconds - ((Date.now() - startOffset) % milliseconds) : milliseconds; + } const runnable = new java.lang.Runnable({ run: () => { zoneBound(); if (timeoutCallbacks[id]) { - handler.postDelayed(runnable, long(milliseconds)); + handler.postDelayed(runnable, long(nextCallMs())); } }, }); @@ -72,7 +76,7 @@ export function setInterval(callback: Function, milliseconds = 0, ...args): numb timeoutCallbacks[id] = runnable; } - timeoutHandler.postDelayed(runnable, long(milliseconds)); + timeoutHandler.postDelayed(runnable, long(nextCallMs())); return id; } From bd7c686aaf55d26b2b483905bd5e0a453429cabd Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Thu, 19 Nov 2020 22:02:32 -0300 Subject: [PATCH 3/5] feat(core): allow app to start without a root view (#9056) --- packages/core/application/application-interfaces.ts | 2 +- packages/core/application/index.android.ts | 2 +- packages/core/application/index.d.ts | 3 ++- packages/core/application/index.ios.ts | 4 +++- packages/core/ui/frame/index.android.ts | 6 ++++++ 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/core/application/application-interfaces.ts b/packages/core/application/application-interfaces.ts index 9427f68a4..13ea7fa5c 100644 --- a/packages/core/application/application-interfaces.ts +++ b/packages/core/application/application-interfaces.ts @@ -20,7 +20,7 @@ export interface ApplicationEventData extends EventData { } export interface LaunchEventData extends ApplicationEventData { - root?: View; + root?: View | null; savedInstanceState?: any /* android.os.Bundle */; } diff --git a/packages/core/application/index.android.ts b/packages/core/application/index.android.ts index c3dbacd38..50ce16597 100644 --- a/packages/core/application/index.android.ts +++ b/packages/core/application/index.android.ts @@ -191,7 +191,7 @@ export function addCss(cssText: string, attributeScoped?: boolean): void { const CALLBACKS = '_callbacks'; export function _resetRootView(entry?: NavigationEntry | string): void { - const activity = androidApp.foregroundActivity; + const activity = androidApp.foregroundActivity || androidApp.startActivity; if (!activity) { throw new Error('Cannot find android activity.'); } diff --git a/packages/core/application/index.d.ts b/packages/core/application/index.d.ts index d2cb041a4..07416d5f8 100644 --- a/packages/core/application/index.d.ts +++ b/packages/core/application/index.d.ts @@ -103,8 +103,9 @@ export interface LaunchEventData extends ApplicationEventData { /** * The root view for this Window on iOS or Activity for Android. * If not set a new Frame will be created as a root view in order to maintain backwards compatibility. + * If explicitly set to null, there will be no root view. */ - root?: View; + root?: View | null; savedInstanceState?: any /* android.os.Bundle */; } diff --git a/packages/core/application/index.ios.ts b/packages/core/application/index.ios.ts index e502b4758..f36cafb70 100644 --- a/packages/core/application/index.ios.ts +++ b/packages/core/application/index.ios.ts @@ -207,7 +207,9 @@ export class iOSApplication implements iOSApplicationDefinition { // this._window will be undefined when NS app is embedded in a native one if (this._window) { - this.setWindowContent(args.root); + if (args.root !== null) { + this.setWindowContent(args.root); + } } else { this._window = UIApplication.sharedApplication.delegate.window; } diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts index 9203131ee..ccfc3a918 100644 --- a/packages/core/ui/frame/index.android.ts +++ b/packages/core/ui/frame/index.android.ts @@ -1301,13 +1301,19 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { if (!rootView) { const mainEntry = application.getMainEntry(); const intent = activity.getIntent(); + // useful for integrations that would like to set rootView asynchronously after app launch + let shouldRootViewBeEmpty = false; if (fireLaunchEvent) { // entry point for Angular and Vue frameworks rootView = notifyLaunch(intent, savedInstanceState, null); + shouldRootViewBeEmpty = rootView === null; } if (!rootView) { + if (shouldRootViewBeEmpty) { + return; + } // entry point for NS Core if (!mainEntry) { // Also handles scenarios with Angular and Vue where the notifyLaunch didn't return a root view. From 44ee97b29d18471afa3185fa4500bdc6f2773f89 Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Tue, 24 Nov 2020 14:47:07 +0100 Subject: [PATCH 4/5] feat: color methods Should fill most needs and remove the need for external libs like tinycolor --- packages/core/color/color-common.ts | 291 ++++++++++++++++++++++++++++ packages/core/color/index.d.ts | 115 +++++++++++ 2 files changed, 406 insertions(+) diff --git a/packages/core/color/color-common.ts b/packages/core/color/color-common.ts index 6c6bd72e9..8408838f3 100644 --- a/packages/core/color/color-common.ts +++ b/packages/core/color/color-common.ts @@ -37,6 +37,11 @@ export class Color implements definition.Color { // The parameter is a 32-bit unsigned integer where each 8 bits specify a color component // In case a 32-bit signed int (Android, Java has no unsigned types) was provided - convert to unsigned by applyint >>> 0 this._argb = arg >>> 0; + } else if (arg && arg._argb) { + // we would go there if a color was passed as an argument (or an object which is why we dont do instanceof) + // The parameter is a 32-bit unsigned integer where each 8 bits specify a color component + // In case a 32-bit signed int (Android, Java has no unsigned types) was provided - convert to unsigned by applyint >>> 0 + this._argb = arg._argb >>> 0; } else { throw new Error('Expected 1 or 4 constructor parameters.'); } @@ -137,6 +142,10 @@ export class Color implements definition.Color { return HEX_REGEX.test(value) || isRgbOrRgba(value) || isHslOrHsla(value); } + public static fromHSL(a, h, s, l) { + const rgb = hslToRgb(h, s, l); + return new Color(a, rgb.r, rgb.g, rgb.b); + } private _componentToHex(component: number): string { let hex = component.toString(16); @@ -163,6 +172,205 @@ export class Color implements definition.Color { public static fromIosColor(value: UIColor): Color { return undefined; } + + /** + * return true if brightenss < 128 + * + */ + public isDark() { + return this.getBrightness() < 128; + } + + /** + * return true if brightenss >= 128 + * + */ + public isLight() { + return !this.isDark(); + } + + /** + * return the [brightness](http://www.w3.org/TR/AERT#color-contrast) + * + */ + public getBrightness() { + return (this.r * 299 + this.g * 587 + this.b * 114) / 1000; + } + + /** + * return the [luminance](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef) + * + */ + public getLuminance() { + let R, G, B; + const RsRGB = this.r / 255; + const GsRGB = this.g/255; + const BsRGB = this.b/255; + + if (RsRGB <= 0.03928) {R = RsRGB / 12.92;} else {R = Math.pow(((RsRGB + 0.055) / 1.055), 2.4);} + if (GsRGB <= 0.03928) {G = GsRGB / 12.92;} else {G = Math.pow(((GsRGB + 0.055) / 1.055), 2.4);} + if (BsRGB <= 0.03928) {B = BsRGB / 12.92;} else {B = Math.pow(((BsRGB + 0.055) / 1.055), 2.4);} + return (0.2126 * R) + (0.7152 * G) + (0.0722 * B); + } + + /** + * Return this color (as a new Color instance) with the provided alpha + * + * @param alpha (between 0 and 255) + */ + public setAlpha(a: number) { + return new Color(a, this.r, this.g, this.b); + } + + /** + * return the hsl representation of the color + * + */ + public toHsl() { + const hsl = rgbToHsl(this.r, this.g, this.b); + return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this.a }; + } + + /** + * return the [CSS hsv](https://www.w3schools.com/Css/css_colors_hsl.asp) representation of the color + * + */ + public toHslString() { + const hsl = rgbToHsl(this.r, this.g, this.b); + const h = Math.round(hsl.h * 360), s = Math.round(hsl.s * 100), l = Math.round(hsl.l * 100); + const a = this.a; + return (a == 255) ? + "hsl(" + h + ", " + s + "%, " + l + "%)" : + "hsla(" + h + ", " + s + "%, " + l + "%, "+ (a/255).toFixed(2) + ")"; + } + + /** + * return the hsv representation of the color + * + */ + public toHsv() { + const hsv = rgbToHsv(this.r, this.g, this.b); + return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this.a }; + } + + /** + * return the [CSS hsv](https://www.w3schools.com/Css/css_colors_rgb.asp) representation of the color + * + */ + public toHsvString() { + const hsv = rgbToHsv(this.r, this.g, this.b); + const h = Math.round(hsv.h * 360), s = Math.round(hsv.s * 100), v = Math.round(hsv.v * 100); + const a = this.a; + return (a == 255) ? + "hsv(" + h + ", " + s + "%, " + v + "%)" : + "hsva(" + h + ", " + s + "%, " + v + "%, "+ (a/255).toFixed(2) + ")"; + } + + /** + * return the [CSS rgb](https://www.w3schools.com/Css/css_colors_rgb.asp) representation of the color + * + */ + public toRgbString() { + const a = this.a; + return (a == 1) ? + "rgb(" + Math.round(this.r) + ", " + Math.round(this.g) + ", " + Math.round(this.b) + ")" : + "rgba(" + Math.round(this.r) + ", " + Math.round(this.g) + ", " + Math.round(this.b) + ", " + (a/255).toFixed(2) + ")"; + } + + /** + * Desaturate the color a given amount, from 0 to 100. Providing 100 will is the same as calling greyscale. + * + * @param amount (between 0 and 100) + */ + public desaturate(amount: number) { + amount = (amount === 0) ? 0 : (amount || 10); + const hsl = this.toHsl(); + hsl.s -= amount / 100; + hsl.s = Math.min(1, Math.max(0, hsl.s)) + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } + + /** + * Saturate the color a given amount, from 0 to 100. + * + * @param amount (between 0 and 100) + */ + public saturate(amount: number) { + amount = (amount === 0) ? 0 : (amount || 10); + const hsl = this.toHsl(); + hsl.s += amount / 100; + hsl.s = Math.min(1, Math.max(0, hsl.s)) + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } + + /** + * Completely desaturates a color into greyscale. Same as calling desaturate(100). + * + */ + public greyscale() { + return this.desaturate(100); + } + + /** + * Lighten the color a given amount, from 0 to 100. Providing 100 will always return white. + * + * @param amount (between 0 and 100) + */ + public lighten (amount: number) { + amount = (amount === 0) ? 0 : (amount || 10); + const hsl = this.toHsl(); + hsl.l += amount / 100; + hsl.l = Math.min(1, Math.max(0, hsl.l)) + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } + + /** + * Brighten the color a given amount, from 0 to 100. + * + * @param amount (between 0 and 100) + */ + public brighten(amount: number) { + amount = (amount === 0) ? 0 : (amount || 10); + const r = Math.max(0, Math.min(255, this.r - Math.round(255 * - (amount / 100)))); + const g = Math.max(0, Math.min(255, this.g - Math.round(255 * - (amount / 100)))); + const b = Math.max(0, Math.min(255, this.b - Math.round(255 * - (amount / 100)))); + return new Color(this.a, r, g, b); + } + + /** + * Darken the color a given amount, from 0 to 100. Providing 100 will always return black. + * + * @param amount (between 0 and 100) + */ + public darken (amount: number) { + amount = (amount === 0) ? 0 : (amount || 10); + const hsl = this.toHsl(); + hsl.l -= amount / 100; + hsl.l = Math.min(1, Math.max(0, hsl.l)) + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } + + /** + * Spin the hue a given amount, from -360 to 360. Calling with 0, 360, or -360 will do nothing (since it sets the hue back to what it was before). + * + * @param amount (between 0 and 100) + */ + public spin(amount: number) { + const hsl = this.toHsl(); + const hue = (hsl.h + amount) % 360; + hsl.h = hue < 0 ? 360 + hue : hue; + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } + + /** + * returns the color complement + * + */ + public complement() { + const hsl = this.toHsl(); + hsl.h = (hsl.h + 180) % 360; + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } } function isRgbOrRgba(value: string): boolean { @@ -222,3 +430,86 @@ function argbFromHslOrHsla(value: string): number { return (a & 0xff) * 0x01000000 + (r & 0xff) * 0x00010000 + (g & 0xff) * 0x00000100 + (b & 0xff); } + +// `rgbToHsl` +// Converts an RGB color value to HSL. +// *Assumes:* r, g, and b are contained in [0, 255] or [0, 1] +// *Returns:* { h, s, l } in [0,1] +function rgbToHsl(r, g, b) { + const max = Math.max(r, g, b), min = Math.min(r, g, b); + let h, s; + const l = (max + min) / 2; + + if(max == min) { + h = s = 0; // achromatic + } + else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch(max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + + h /= 6; + } + + return { h: h, s: s, l: l }; +} + +function hue2rgb(p, q, t) { + if(t < 0) t += 1; + if(t > 1) t -= 1; + if(t < 1/6) return p + (q - p) * 6 * t; + if(t < 1/2) return q; + if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; +} + +// `hslToRgb` +// Converts an HSL color value to RGB. +// *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100] +// *Returns:* { r, g, b } in the set [0, 255] +function hslToRgb(h, s, l) { + let r, g, b; + if(s === 0) { + r = g = b = l; // achromatic + } + else { + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return { r: r * 255, g: g * 255, b: b * 255 }; +} + + +// `rgbToHsv` +// Converts an RGB color value to HSV +// *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1] +// *Returns:* { h, s, v } in [0,1] +function rgbToHsv(r, g, b) { + const max = Math.max(r, g, b), min = Math.min(r, g, b); + let h; + const v = max; + + const d = max - min; + const s = max === 0 ? 0 : d / max; + + if(max == min) { + h = 0; // achromatic + } + else { + switch(max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h, s: s, v: v }; +} diff --git a/packages/core/color/index.d.ts b/packages/core/color/index.d.ts index e6529fb21..56ac39610 100644 --- a/packages/core/color/index.d.ts +++ b/packages/core/color/index.d.ts @@ -75,4 +75,119 @@ export class Color { * Creates color from iOS-specific UIColor value representation. */ public static fromIosColor(value: any /* UIColor */): Color; + + /** + * return true if brightenss < 128 + * + */ + public isDark(): boolean; + + /** + * return true if brightenss >= 128 + * + */ + public isLight(): boolean; + + /** + * return the [brightness](http://www.w3.org/TR/AERT#color-contrast) + * + */ + public getBrightness(): number; + /** + * return the [luminance](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef) + * + */ + public getLuminance(): number; + + /** + * Return this color (as a new Color instance) with the provided alpha + * + * @param alpha (between 0 and 255) + */ + public setAlpha(a: number): Color; + /** + * return the hsl representation of the color + * + */ + public toHsl() : { h: number, s: number, l: number, a: number }; + + /** + * return the [CSS hsv](https://www.w3schools.com/Css/css_colors_hsl.asp) representation of the color + * + */ + public toHslString(): string; + + /** + * return the hsv representation of the color + * + */ + public toHsv(): { h: number, s: number, v: number, a: number }; + + /** + * return the [CSS hsv](https://www.w3schools.com/Css/css_colors_rgb.asp) representation of the color + * + */ + public toHsvString(): string; + + /** + * return the [CSS rgb](https://www.w3schools.com/Css/css_colors_rgb.asp) representation of the color + * + */ + public toRgbString(): string; + + /** + * Desaturate the color a given amount, from 0 to 100. Providing 100 will is the same as calling greyscale. + * + * @param amount (between 0 and 100) + */ + public desaturate(amount: number): Color; + + /** + * Saturate the color a given amount, from 0 to 100. + * + * @param amount (between 0 and 100) + */ + public saturate(amount: number): Color; + + /** + * Completely desaturates a color into greyscale. Same as calling desaturate(100). + * + * @returns + */ + public greyscale(): Color; + + /** + * Lighten the color a given amount, from 0 to 100. Providing 100 will always return white. + * + * @param amount (between 0 and 100) + * @returns olor : Color + */ + public lighten (amount: number): Color; + + /** + * Brighten the color a given amount, from 0 to 100. + * + * @param amount (between 0 and 100) + */ + public brighten(amount: number): Color; + + /** + * Darken the color a given amount, from 0 to 100. Providing 100 will always return black. + * + * @param amount (between 0 and 100) + */ + public darken (amount: number): Color; + + /** + * Spin the hue a given amount, from -360 to 360. Calling with 0, 360, or -360 will do nothing (since it sets the hue back to what it was before). + * + * @param amount (between 0 and 100) + */ + public spin(amount: number): Color; + + /** + * returns the color complement + * + */ + public complement(): Color; } From aa9c5d15842a67f5d1f1484279c5fcd69e54e297 Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Sat, 28 Nov 2020 13:33:31 +0100 Subject: [PATCH 5/5] typo fix --- packages/core/color/color-common.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/color/color-common.ts b/packages/core/color/color-common.ts index 8408838f3..8942e3f07 100644 --- a/packages/core/color/color-common.ts +++ b/packages/core/color/color-common.ts @@ -174,7 +174,7 @@ export class Color implements definition.Color { } /** - * return true if brightenss < 128 + * return true if brightness < 128 * */ public isDark() { @@ -182,7 +182,7 @@ export class Color implements definition.Color { } /** - * return true if brightenss >= 128 + * return true if brightness >= 128 * */ public isLight() {