diff --git a/CHANGELOG.md b/CHANGELOG.md index c38dbe09..93853468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## v2.9.0 +*16 feb 2023* + +- Fixed issues related to package.json exports (#434) +- Added a warning about `strictNullChecks` option to the TypeScript docs (#433) +- Fixed an issue occurring while using `maxLinesSuffix` when `advancedRender` is set to `true` (#429) +- Fixed an alignment issue occurring when the advanced text renderer is used (#428) +- Added an example of basic subclassing to the TypeScript docs (#446) +- Fixed an issue related to an inconsistency in the handling of default fonts +- Added instant freeing up of text textures to prevent memory building up when text is being changed +- Updated docs to add `letterSpacing` property to `Text` texture + ## v2.8.1 *31 oct 2022* diff --git a/docs/RenderEngine/Textures/Text.md b/docs/RenderEngine/Textures/Text.md index ed070ff9..7ab48a9d 100644 --- a/docs/RenderEngine/Textures/Text.md +++ b/docs/RenderEngine/Textures/Text.md @@ -42,6 +42,8 @@ You can use various properties to control the way in which you want to render te | `cutEx` | Integer | 0 | x coordinate of text cutting ending position | | `cutSy` | Integer | 0 | y coordinate of text cutting starting position | | `cutEy` | Integer | 0 | y coordinate of text cutting ending position | +| `letterSpacing` | Integer | 0 | Letter spacing of characters | + @@ -99,4 +101,4 @@ class TextDemo extends lng.Application { const options = {stage: {w: window.innerWidth, h: window.innerHeight, useImageWorker: false}}; const App = new TextDemo(options); document.body.appendChild(App.stage.getCanvas()); -``` \ No newline at end of file +``` diff --git a/docs/TypeScript/Components/SubclassableComponents.md b/docs/TypeScript/Components/SubclassableComponents.md index ce8e78e2..c978b6f8 100644 --- a/docs/TypeScript/Components/SubclassableComponents.md +++ b/docs/TypeScript/Components/SubclassableComponents.md @@ -1,6 +1,80 @@ # Subclassable Components -Usually, when creating a Lightning Component you do not need to specify your own generic parameters which are mixed into the ones passed further down into the Component base class. However, there are times when you want to make your Component type flexible and perhaps just as flexible as the Component base class which allows you to pass a [Template Spec](./TemplateSpecs.md) and [Type Config](./TypeConfig.md). Perhaps you want to create a `MyList` component that accepts any child Component of type T. Or a completely flexible `MyBaseComponent` which all of your App's Components extend. When doing this there are a few guidelines and gotchas you need to pay attention to that stem from some design choices in TypeScript. +Usually, when creating a Lightning Component you do not need to specify your own generic parameters which are mixed into the ones passed further down into the Component base class. However, there are times when you want to make your Component type flexible and perhaps just as flexible as the Component base class which allows you to pass a [Template Spec](./TemplateSpecs.md) and [Type Config](./TypeConfig.md). Perhaps you want to create a `PageBase` component that accepts any child Component of type T. Or a completely flexible `MyBaseComponent` which all of your App's Components extend. When doing this there are a few guidelines and gotchas you need to pay attention to that stem from some design choices in TypeScript. + +## Basic Generic Ref Type + +If we want to create an extendible base Component called `PageBase`, where `T` is the type of Component used for the content of a page appearing with a static header component, we start by laying down the definition of its Template Spec like so: + +```ts +export interface PageTemplateSpec< + T extends Lightning.Component.Constructor = Lightning.Component.Constructor, +> extends Lightning.Component.TemplateSpec { + Header: typeof Header + Content: T +} +``` + +`Lightning.Component.Constructor` represents a `typeof` any Lightning Component. For example, if there's a component called `List` you can use `typeof List` as the type argument for `T`. + +Then we implement the `PageBase` class itself using the same generic type parameter passed down into the Template Spec: + +```ts +export class PageBase + extends Lightning.Component> + implements Lightning.Component.ImplementTemplateSpec> +{ + static override _template(): Lightning.Component.Template { + return { + w: (w: number) => w, + h: (h: number) => h, + rect: true, + color: 0xff0e0e0e, + + Header: { + type: Header, + }, + Content: undefined, + }; + } + + Content = this.getByRef('Content')!; +} +``` + +We fill the base `_template()` out with how we want our base component to appear. Here a Header component is provided and we leave the Content component intentionally `undefined`. It will be provided by the subclass implementation. We also create a read-only property for `Content` which returns the result of `getByRef('Content')`. + +Notice how the return type of `_template()` does not reference `T`. Due to how class generics work in TypeScript you are not allowed to pass the generic parameters from the class definition. By leaving it out we open it to be used for any Lightning Component type. + +With this base we are ready to implement a subclass. Here we use a component called `List` as our Content component: + +```ts +export class Discovery extends BasePage { + static override _template(): Lightning.Component.Template> { + // Must assert the specific template type to the type of the template spec + // because `super._template()` isn't/can't be aware of List + const pageTemplate = super._template() as Lightning.Component.Template< + IPageTemplateSpec + >; + + pageTemplate.Content = { + type: List, + w: (w: number) => w, + h: (h: number) => h, + }; + + return pageTemplate; + } + + override _init() { + this.Content.someListSpecificProperty = false; + } +} +``` + +Here the `_template()` method calls the base component's `_template()` method, and then supplements it's Content ref with the `List` component. We must use `as` to assert the correct final type of the template object because of what we said above about class generic parameters and static methods. From here, you should be able to reference `this.Content` in your component's implementation and it will automatically be resolved to an instance of `List`. + +## Extendible TemplateSpec / TypeConfig If we wish to create our own `MyBaseComponent` that provides its own base Template Spec and Event Map. First we start by creating our own interfaces for Template Spec and Type Config that extend their respective base interfaces from Component. The wrapping namespace `MyBaseComponent` is used purely for convention and organization. It is recommended but not required. diff --git a/docs/TypeScript/index.md b/docs/TypeScript/index.md index d25f04ea..a2d3f6cb 100644 --- a/docs/TypeScript/index.md +++ b/docs/TypeScript/index.md @@ -41,6 +41,8 @@ TypeScript projects must include a [TSConfig file](https://www.typescriptlang.or } ``` +**Warning:** At a minimum, `strictNullChecks` [must be set to true](https://github.com/rdkcentral/Lightning-SDK/issues/358#issuecomment-1339321527) to avoid certain errors. This is done implicitly by setting `strict` to true as in the above configuration. + ### Importing Lightning should only be imported from a single import as such: diff --git a/package-lock.json b/package-lock.json index e98c0da3..feeeee22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lightningjs/core", - "version": "2.8.1", + "version": "2.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lightningjs/core", - "version": "2.8.1", + "version": "2.9.0", "license": "Apache-2.0", "devDependencies": { "@babel/core": "^7.8.3", diff --git a/package.json b/package.json index 34c27b9f..a309ad20 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "Metrological, Bas van Meurs ", "name": "@lightningjs/core", - "version": "2.8.1", + "version": "2.9.0", "license": "Apache-2.0", "main": "dist/lightning.js", "module": "index.js", @@ -13,10 +13,11 @@ "default": "./index.js" }, "require": { - "types": "./dist/lightning.js", - "default": "./index.d.ts" + "types": "./index.d.ts", + "default": "./dist/lightning.js" } - } + }, + "./package.json": "./package.json" }, "files": [ "tsconfig.json", @@ -55,4 +56,4 @@ "tsd": "^0.21.0", "typedoc": "^0.23.10" } -} +} \ No newline at end of file diff --git a/src/textures/TextTexture.mjs b/src/textures/TextTexture.mjs index 99d50eb8..6d0d592c 100644 --- a/src/textures/TextTexture.mjs +++ b/src/textures/TextTexture.mjs @@ -534,11 +534,6 @@ export default class TextTexture extends Texture { _getSourceLoader() { const args = this.cloneArgs(); - // Inherit font face from stage. - if (args.fontFace === null) { - args.fontFace = this.stage.getOption('defaultFontFace'); - } - const gl = this.stage.gl; return function (cb) { diff --git a/src/textures/TextTextureRenderer.mjs b/src/textures/TextTextureRenderer.mjs index a71233ff..87a07737 100644 --- a/src/textures/TextTextureRenderer.mjs +++ b/src/textures/TextTextureRenderer.mjs @@ -19,6 +19,7 @@ import StageUtils from "../tree/StageUtils.mjs"; import Utils from "../tree/Utils.mjs"; +import { getFontSetting } from "./TextTextureRendererUtils.mjs"; export default class TextTextureRenderer { @@ -34,32 +35,25 @@ export default class TextTextureRenderer { }; setFontProperties() { - this._context.font = Utils.isSpark ? this._stage.platform.getFontSetting(this) : this._getFontSetting(); + this._context.font = getFontSetting( + this._settings.fontFace, + this._settings.fontStyle, + this._settings.fontSize, + this.getPrecision(), + this._stage.getOption('defaultFontFace'), + ); this._context.textBaseline = this._settings.textBaseline; }; - _getFontSetting() { - let ff = this._settings.fontFace; - - if (!Array.isArray(ff)) { - ff = [ff]; - } - - let ffs = []; - for (let i = 0, n = ff.length; i < n; i++) { - if (ff[i] === "serif" || ff[i] === "sans-serif") { - ffs.push(ff[i]); - } else { - ffs.push(`"${ff[i]}"`); - } - } - - return `${this._settings.fontStyle} ${this._settings.fontSize * this.getPrecision()}px ${ffs.join(",")}` - } - _load() { if (Utils.isWeb && document.fonts) { - const fontSetting = this._getFontSetting(); + const fontSetting = getFontSetting( + this._settings.fontFace, + this._settings.fontStyle, + this._settings.fontSize, + this.getPrecision(), + this._stage.getOption('defaultFontFace') + ); try { if (!document.fonts.check(fontSetting, this._settings.text)) { // Use a promise that waits for loading. @@ -471,5 +465,5 @@ export default class TextTextureRenderer { return acc + this._context.measureText(char).width + space; }, 0); } - + } diff --git a/src/textures/TextTextureRendererAdvanced.mjs b/src/textures/TextTextureRendererAdvanced.mjs index bf9ba953..7e12d7af 100644 --- a/src/textures/TextTextureRendererAdvanced.mjs +++ b/src/textures/TextTextureRendererAdvanced.mjs @@ -19,6 +19,7 @@ import StageUtils from "../tree/StageUtils.mjs"; import Utils from "../tree/Utils.mjs"; +import { getFontSetting } from "./TextTextureRendererUtils.mjs"; export default class TextTextureRendererAdvanced { @@ -34,34 +35,27 @@ export default class TextTextureRendererAdvanced { }; setFontProperties() { - const font = Utils.isSpark ? this._stage.platform.getFontSetting(this) : this._getFontSetting(); + const font = getFontSetting( + this._settings.fontFace, + this._settings.fontStyle, + this._settings.fontSize, + this.getPrecision(), + this._stage.getOption('defaultFontFace') + ); this._context.font = font; this._context.textBaseline = this._settings.textBaseline; return font; }; - _getFontSetting() { - let ff = this._settings.fontFace; - - if (!Array.isArray(ff)) { - ff = [ff]; - } - - let ffs = []; - for (let i = 0, n = ff.length; i < n; i++) { - if (ff[i] === "serif" || ff[i] === "sans-serif") { - ffs.push(ff[i]); - } else { - ffs.push(`"${ff[i]}"`); - } - } - - return `${this._settings.fontStyle} ${this._settings.fontSize * this.getPrecision()}px ${ffs.join(",")}` - } - _load() { if (Utils.isWeb && document.fonts) { - const fontSetting = this._getFontSetting(); + const fontSetting = getFontSetting( + this._settings.fontFace, + this._settings.fontStyle, + this._settings.fontSize, + this.getPrecision(), + this._stage.getOption('defaultFontFace') + ); try { if (!document.fonts.check(fontSetting, this._settings.text)) { // Use a promise that waits for loading. @@ -207,12 +201,6 @@ export default class TextTextureRendererAdvanced { renderInfo.h = this._settings.h; } else if (renderInfo.maxLines && renderInfo.maxLines < renderInfo.lineNum) { renderInfo.h = renderInfo.maxLines * renderInfo.lineHeight + fontSize / 2; - } else if (renderInfo.lineHeight > fontSize) { - // When lineheight is larger than the font size we're rendering, we set the height of the canvas based on the number of lines we're rendering. - // This makes each "line" a containing box that is line height sized, and text is positioned inside that box. - // - // Ideographic fonts may break this model, and require additional space? - renderInfo.h = renderInfo.lineNum * renderInfo.lineHeight } else { renderInfo.h = renderInfo.lineNum * renderInfo.lineHeight + fontSize / 2; } @@ -283,25 +271,23 @@ export default class TextTextureRendererAdvanced { let suffix = renderInfo.maxLinesSuffix; suffix = this.tokenize(suffix); suffix = this.parse(suffix); - suffix = this.measure(suffix, renderInfo.letterSpacing, renderInfo.baseFont)[0]; - suffix.lineNo = index; - if (lastLineText.length) { - suffix.x = lastLineText[lastLineText.length - 1].x + lastLineText[lastLineText.length - 1].width; - } else { - suffix.x = 0; + suffix = this.measure(suffix, renderInfo.letterSpacing, renderInfo.baseFont); + for (const s of suffix) { + s.lineNo = index; + s.x = 0; + lastLineText.push(s) } - lastLineText.push(suffix) + const spl = suffix.length + 1 let _w = lastLineText.reduce((acc, t) => acc + t.width, 0); - while (_w > renderInfo.width || lastLineText[lastLineText.length - 2].text == ' ') { - lastLineText.splice(lastLineText.length - 2, 1); + while (_w > renderInfo.width || lastLineText[lastLineText.length - spl].text == ' ') { + lastLineText.splice(lastLineText.length - spl, 1); _w = lastLineText.reduce((acc, t) => acc + t.width, 0); - const prev = lastLineText[lastLineText.length - 2] || {x: 0, width: 0} - suffix.x = prev.x + prev.width; - if (lastLineText.length < 2) { + if (lastLineText.length < spl) { break; } } + this.alignLine(lastLineText, lastLineText[0].x) renderInfo.lines[index].text = lastLineText; renderInfo.lines[index].width = _w; @@ -364,7 +350,7 @@ export default class TextTextureRendererAdvanced { const hlPaddingRight = (renderInfo.highlightPaddingRight !== null ? renderInfo.highlightPaddingRight * precision : renderInfo.paddingRight); this._context.fillStyle = StageUtils.getRgbaString(hlColor); - const lineNum = renderInfo.maxLines ? Math.min(renderInfo.maxLines, renderInfo.lineNum) : renderInfo.lineNum; + const lineNum = renderInfo.maxLines ? Math.min(renderInfo.maxLines, renderInfo.lineNum) : renderInfo.lineNum; for (let i = 0; i < lineNum; i++) { const l = renderInfo.lines[i]; this._context.fillRect(l.x - hlPaddingLeft + paddingLeft, l.y + hlOffset, l.width + hlPaddingLeft + hlPaddingRight, hlHeight); @@ -432,7 +418,7 @@ export default class TextTextureRendererAdvanced { if (renderInfo.cutSx || renderInfo.cutSy) { this._context.translate(renderInfo.cutSx, renderInfo.cutSy); } - + // Postprocess renderInfo.lines to be compatible with standard version renderInfo.lines = renderInfo.lines.map((l) => l.text.reduce((acc, v) => acc + v.text, '')); if (renderInfo.maxLines) { @@ -455,19 +441,19 @@ export default class TextTextureRendererAdvanced { tokenize(text) { const re =/ |\n||<\/i>||<\/b>||<\/color>/g - + const delimeters = text.match(re) || []; const words = text.split(re) || []; - + let final = []; for (let i = 0; i < words.length; i++) { final.push(words[i], delimeters[i]) } final.pop() return final.filter((word) => word != ''); - + } - + parse(tokens) { let italic = 0; let bold = 0; @@ -475,7 +461,7 @@ export default class TextTextureRendererAdvanced { let color = 0; const colorRegexp = //; - + return tokens.map((t) => { if (t == '') { italic += 1; @@ -689,4 +675,18 @@ export default class TextTextureRendererAdvanced { return parts; } + + alignLine(parsed, initialX = 0) { + let prevWidth = 0; + let prevX = initialX; + for (const word of parsed) { + if (word.text == '\n') { + continue; + } + word.x = prevX + prevWidth; + prevX = word.x; + prevWidth = word.width; + } + + } } \ No newline at end of file diff --git a/src/textures/TextTextureRendererUtils.mjs b/src/textures/TextTextureRendererUtils.mjs new file mode 100644 index 00000000..3e19f714 --- /dev/null +++ b/src/textures/TextTextureRendererUtils.mjs @@ -0,0 +1,35 @@ +/** + * Returns CSS font setting string for use in canvas context. + * + * @private + * @param {string | string[]} fontFace + * @param {string} fontStyle + * @param {number} fontSize + * @param {number} precision + * @param {string} defaultFontFace + * @returns {string} + */ +export function getFontSetting(fontFace, fontStyle, fontSize, precision, defaultFontFace) { + let ff = fontFace; + + if (!Array.isArray(ff)) { + ff = [ff]; + } + + let ffs = []; + for (let i = 0, n = ff.length; i < n; i++) { + let curFf = ff[i]; + // Replace the default font face `null` with the actual default font face set + // on the stage. + if (curFf === null) { + curFf = defaultFontFace; + } + if (curFf === "serif" || curFf === "sans-serif") { + ffs.push(curFf); + } else { + ffs.push(`"${curFf}"`); + } + } + + return `${fontStyle} ${fontSize * precision}px ${ffs.join(",")}` +} diff --git a/src/tree/Texture.mjs b/src/tree/Texture.mjs index 0ea1fff4..39ca10e0 100644 --- a/src/tree/Texture.mjs +++ b/src/tree/Texture.mjs @@ -326,6 +326,11 @@ export default class Texture { } oldSource.removeTexture(this); + + // free up unused TextTextures immediately as they are not reused anyway + if(this['text'] && !oldSource.isUsed()) { + this.manager.freeTextureSource(oldSource); + } } if (newSource) {