From 95d353a7496e638180b07bc17b6ad7443913b365 Mon Sep 17 00:00:00 2001 From: Ben Whittaker Date: Tue, 14 Sep 2021 20:02:09 -0400 Subject: [PATCH 1/4] Add lazy rendering support to Layout --- modules/Layout.js | 43 +++++++++++++++++++++++++++++++++++++++---- modules/Layout.min.js | 43 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/modules/Layout.js b/modules/Layout.js index a45b17107..ad033bb4d 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -1,11 +1,10 @@ - /* Usage: ``` var Layout = require("Layout"); -var layout = new Layout( layoutObject, btns ) +var layout = new Layout( layoutObject, btns, options ) layout.render(optionalObject); ``` @@ -52,6 +51,13 @@ btns is an array of objects containing: * `cb` - a callback function * `cbl` - a callback function for long presses +options is an object containing: + +* `lazy` - a boolean specifying whether to enable automatic lazy rendering + +If automatic lazy rendering is enabled, calls to `layout.render()` will attempt to automatically +determine what objects have changed or moved, clear their previous locations, and re-render just those objects. + Once `layout.update()` is called, the following fields are added to each object: @@ -69,13 +75,16 @@ Other functions: */ -function Layout(layout, buttons) { +function Layout(layout, buttons, options) { this._l = this.l = layout; this.b = buttons; // Do we have >1 physical buttons? this.physBtns = (process.env.HWVERSION==2) ? 1 : 3; this.yOffset = Object.keys(global.WIDGETS).length ? 24 : 0; + options = options || {}; + this.lazy = options.lazy || false; + if (buttons) { if (this.physBtns >= buttons.length) { // enough physical buttons @@ -234,9 +243,35 @@ function render(l) { if (l.c) l.c.forEach(render); } +function prepareLazyRender(l, rectsToClear, drawList) { + if (l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") { + let hash = E.CRC32(E.toJS(l)); + if (!delete rectsToClear[hash]) drawList.push({hash: hash, l: l}); + } + else if (l.c) l.c.forEach(l => prepareLazyRender(l, rectsToClear, drawList)); +} + Layout.prototype.render = function (l) { if (!l) l = this._l; - render(l); + + if (this.lazy) { + // TODO: Handle bg colors + + if (!this.rects) this.rects = {}; + let rectsToClear = Object.assign({}, this.rects); + let drawList = []; + prepareLazyRender(l, rectsToClear, drawList); + for (let hash in rectsToClear) delete this.rects[hash]; + for (let rect of rectsToClear) if (rect) g.clearRect(rect.x1, rect.y1, rect.x2, rect.y2); + g.getModified(true); + for (let d of drawList) { + render(d.l); + this.rects[d.hash] = g.getModified(true); + } + } + else { + render(l); + } }; Layout.prototype.layout = function (l) { diff --git a/modules/Layout.min.js b/modules/Layout.min.js index a45b17107..ad033bb4d 100644 --- a/modules/Layout.min.js +++ b/modules/Layout.min.js @@ -1,11 +1,10 @@ - /* Usage: ``` var Layout = require("Layout"); -var layout = new Layout( layoutObject, btns ) +var layout = new Layout( layoutObject, btns, options ) layout.render(optionalObject); ``` @@ -52,6 +51,13 @@ btns is an array of objects containing: * `cb` - a callback function * `cbl` - a callback function for long presses +options is an object containing: + +* `lazy` - a boolean specifying whether to enable automatic lazy rendering + +If automatic lazy rendering is enabled, calls to `layout.render()` will attempt to automatically +determine what objects have changed or moved, clear their previous locations, and re-render just those objects. + Once `layout.update()` is called, the following fields are added to each object: @@ -69,13 +75,16 @@ Other functions: */ -function Layout(layout, buttons) { +function Layout(layout, buttons, options) { this._l = this.l = layout; this.b = buttons; // Do we have >1 physical buttons? this.physBtns = (process.env.HWVERSION==2) ? 1 : 3; this.yOffset = Object.keys(global.WIDGETS).length ? 24 : 0; + options = options || {}; + this.lazy = options.lazy || false; + if (buttons) { if (this.physBtns >= buttons.length) { // enough physical buttons @@ -234,9 +243,35 @@ function render(l) { if (l.c) l.c.forEach(render); } +function prepareLazyRender(l, rectsToClear, drawList) { + if (l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") { + let hash = E.CRC32(E.toJS(l)); + if (!delete rectsToClear[hash]) drawList.push({hash: hash, l: l}); + } + else if (l.c) l.c.forEach(l => prepareLazyRender(l, rectsToClear, drawList)); +} + Layout.prototype.render = function (l) { if (!l) l = this._l; - render(l); + + if (this.lazy) { + // TODO: Handle bg colors + + if (!this.rects) this.rects = {}; + let rectsToClear = Object.assign({}, this.rects); + let drawList = []; + prepareLazyRender(l, rectsToClear, drawList); + for (let hash in rectsToClear) delete this.rects[hash]; + for (let rect of rectsToClear) if (rect) g.clearRect(rect.x1, rect.y1, rect.x2, rect.y2); + g.getModified(true); + for (let d of drawList) { + render(d.l); + this.rects[d.hash] = g.getModified(true); + } + } + else { + render(l); + } }; Layout.prototype.layout = function (l) { From 6bd606b64524b0ddb258b4df2d8f54b449311799 Mon Sep 17 00:00:00 2001 From: Ben Whittaker Date: Wed, 15 Sep 2021 05:58:04 -0400 Subject: [PATCH 2/4] Don't use getModified in lazy layout rendering --- modules/Layout.js | 21 ++++++++++----------- modules/Layout.min.js | 21 ++++++++++----------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/modules/Layout.js b/modules/Layout.js index ad033bb4d..0f1d69f8f 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -214,7 +214,7 @@ function render(l) { if (!l) l = this.l; g.reset(); if (l.col) g.setColor(l.col); - if (l.bgCol!==undefined) g.setBgColor(l.bgCol).clearRect(l.x,l.y,l.x+l.w,l.y+l.h); + if (l.bgCol!==undefined) g.setBgColor(l.bgCol).clearRect(l.x,l.y,l.x+l.w-1,l.y+l.h-1); switch (l.type) { case "txt": g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1), true/*solid bg*/); @@ -243,12 +243,15 @@ function render(l) { if (l.c) l.c.forEach(render); } -function prepareLazyRender(l, rectsToClear, drawList) { +function prepareLazyRender(l, rectsToClear, drawList, rects) { if (l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") { let hash = E.CRC32(E.toJS(l)); - if (!delete rectsToClear[hash]) drawList.push({hash: hash, l: l}); + if (!delete rectsToClear[hash]) { + drawList.push(l); + rects[hash] = [l.x,l.y,l.x+l.w-1,l.y+l.h-1]; + } } - else if (l.c) l.c.forEach(l => prepareLazyRender(l, rectsToClear, drawList)); + else if (l.c) l.c.forEach(l => prepareLazyRender(l, rectsToClear, drawList, rects)); } Layout.prototype.render = function (l) { @@ -260,14 +263,10 @@ Layout.prototype.render = function (l) { if (!this.rects) this.rects = {}; let rectsToClear = Object.assign({}, this.rects); let drawList = []; - prepareLazyRender(l, rectsToClear, drawList); + prepareLazyRender(l, rectsToClear, drawList, this.rects); for (let hash in rectsToClear) delete this.rects[hash]; - for (let rect of rectsToClear) if (rect) g.clearRect(rect.x1, rect.y1, rect.x2, rect.y2); - g.getModified(true); - for (let d of drawList) { - render(d.l); - this.rects[d.hash] = g.getModified(true); - } + for (let rect of rectsToClear) g.clearRect.apply(g, rect); + drawList.forEach(render); } else { render(l); diff --git a/modules/Layout.min.js b/modules/Layout.min.js index ad033bb4d..0f1d69f8f 100644 --- a/modules/Layout.min.js +++ b/modules/Layout.min.js @@ -214,7 +214,7 @@ function render(l) { if (!l) l = this.l; g.reset(); if (l.col) g.setColor(l.col); - if (l.bgCol!==undefined) g.setBgColor(l.bgCol).clearRect(l.x,l.y,l.x+l.w,l.y+l.h); + if (l.bgCol!==undefined) g.setBgColor(l.bgCol).clearRect(l.x,l.y,l.x+l.w-1,l.y+l.h-1); switch (l.type) { case "txt": g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1), true/*solid bg*/); @@ -243,12 +243,15 @@ function render(l) { if (l.c) l.c.forEach(render); } -function prepareLazyRender(l, rectsToClear, drawList) { +function prepareLazyRender(l, rectsToClear, drawList, rects) { if (l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") { let hash = E.CRC32(E.toJS(l)); - if (!delete rectsToClear[hash]) drawList.push({hash: hash, l: l}); + if (!delete rectsToClear[hash]) { + drawList.push(l); + rects[hash] = [l.x,l.y,l.x+l.w-1,l.y+l.h-1]; + } } - else if (l.c) l.c.forEach(l => prepareLazyRender(l, rectsToClear, drawList)); + else if (l.c) l.c.forEach(l => prepareLazyRender(l, rectsToClear, drawList, rects)); } Layout.prototype.render = function (l) { @@ -260,14 +263,10 @@ Layout.prototype.render = function (l) { if (!this.rects) this.rects = {}; let rectsToClear = Object.assign({}, this.rects); let drawList = []; - prepareLazyRender(l, rectsToClear, drawList); + prepareLazyRender(l, rectsToClear, drawList, this.rects); for (let hash in rectsToClear) delete this.rects[hash]; - for (let rect of rectsToClear) if (rect) g.clearRect(rect.x1, rect.y1, rect.x2, rect.y2); - g.getModified(true); - for (let d of drawList) { - render(d.l); - this.rects[d.hash] = g.getModified(true); - } + for (let rect of rectsToClear) g.clearRect.apply(g, rect); + drawList.forEach(render); } else { render(l); From 74e739d019e23623ebaa2d1c4bb59a7fc8bf9b2b Mon Sep 17 00:00:00 2001 From: Ben Whittaker Date: Wed, 15 Sep 2021 09:36:17 -0400 Subject: [PATCH 3/4] Handle bg colors correctly in lazy layout render --- modules/Layout.js | 36 +++++++++++++++++++++++------------- modules/Layout.min.js | 36 +++++++++++++++++++++++------------- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/modules/Layout.js b/modules/Layout.js index 0f1d69f8f..e0d15f8e5 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -243,29 +243,39 @@ function render(l) { if (l.c) l.c.forEach(render); } -function prepareLazyRender(l, rectsToClear, drawList, rects) { - if (l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") { +function prepareLazyRender(l, rectsToClear, drawList, rects, bgCol) { + if ((l.bgCol != null && l.bgCol != bgCol) || l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") { + // Hash the layoutObject without including its children + let c = l.c; + delete l.c; let hash = E.CRC32(E.toJS(l)); - if (!delete rectsToClear[hash]) { - drawList.push(l); - rects[hash] = [l.x,l.y,l.x+l.w-1,l.y+l.h-1]; + if (c) l.c = c; + + let i = rectsToClear.findIndex(r => r.h == hash); + if (i != -1) rectsToClear.splice(i, 1); + else { + rects.push({h: hash, bg: bgCol, x1: l.x, y1: l.y, x2: l.x+l.w-1, y2: l.y+l.h-1}); + if (drawList) { + drawList.push(l); + drawList = null; // Prevent children from being redundantly added to the drawList + } } } - else if (l.c) l.c.forEach(l => prepareLazyRender(l, rectsToClear, drawList, rects)); + + if (l.c) for (let ch of l.c) prepareLazyRender(ch, rectsToClear, drawList, rects, l.bgCol == null ? bgCol : l.bgCol); } Layout.prototype.render = function (l) { if (!l) l = this._l; if (this.lazy) { - // TODO: Handle bg colors - - if (!this.rects) this.rects = {}; - let rectsToClear = Object.assign({}, this.rects); + if (!this.rects) this.rects = []; + let rectsToClear = this.rects.slice(); let drawList = []; - prepareLazyRender(l, rectsToClear, drawList, this.rects); - for (let hash in rectsToClear) delete this.rects[hash]; - for (let rect of rectsToClear) g.clearRect.apply(g, rect); + prepareLazyRender(l, rectsToClear, drawList, this.rects, g.getBgColor()); + this.rects = this.rects.filter(r1 => !rectsToClear.some(r2 => r1.h == r2.h)); + rectsToClear.reverse(); // Rects are cleared in reverse order so that the original bg color is restored + for (let r of rectsToClear) g.setBgColor(r.bg).clearRect(r.x1, r.y1, r.x2, r.y2); drawList.forEach(render); } else { diff --git a/modules/Layout.min.js b/modules/Layout.min.js index 0f1d69f8f..e0d15f8e5 100644 --- a/modules/Layout.min.js +++ b/modules/Layout.min.js @@ -243,29 +243,39 @@ function render(l) { if (l.c) l.c.forEach(render); } -function prepareLazyRender(l, rectsToClear, drawList, rects) { - if (l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") { +function prepareLazyRender(l, rectsToClear, drawList, rects, bgCol) { + if ((l.bgCol != null && l.bgCol != bgCol) || l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") { + // Hash the layoutObject without including its children + let c = l.c; + delete l.c; let hash = E.CRC32(E.toJS(l)); - if (!delete rectsToClear[hash]) { - drawList.push(l); - rects[hash] = [l.x,l.y,l.x+l.w-1,l.y+l.h-1]; + if (c) l.c = c; + + let i = rectsToClear.findIndex(r => r.h == hash); + if (i != -1) rectsToClear.splice(i, 1); + else { + rects.push({h: hash, bg: bgCol, x1: l.x, y1: l.y, x2: l.x+l.w-1, y2: l.y+l.h-1}); + if (drawList) { + drawList.push(l); + drawList = null; // Prevent children from being redundantly added to the drawList + } } } - else if (l.c) l.c.forEach(l => prepareLazyRender(l, rectsToClear, drawList, rects)); + + if (l.c) for (let ch of l.c) prepareLazyRender(ch, rectsToClear, drawList, rects, l.bgCol == null ? bgCol : l.bgCol); } Layout.prototype.render = function (l) { if (!l) l = this._l; if (this.lazy) { - // TODO: Handle bg colors - - if (!this.rects) this.rects = {}; - let rectsToClear = Object.assign({}, this.rects); + if (!this.rects) this.rects = []; + let rectsToClear = this.rects.slice(); let drawList = []; - prepareLazyRender(l, rectsToClear, drawList, this.rects); - for (let hash in rectsToClear) delete this.rects[hash]; - for (let rect of rectsToClear) g.clearRect.apply(g, rect); + prepareLazyRender(l, rectsToClear, drawList, this.rects, g.getBgColor()); + this.rects = this.rects.filter(r1 => !rectsToClear.some(r2 => r1.h == r2.h)); + rectsToClear.reverse(); // Rects are cleared in reverse order so that the original bg color is restored + for (let r of rectsToClear) g.setBgColor(r.bg).clearRect(r.x1, r.y1, r.x2, r.y2); drawList.forEach(render); } else { From 0c4ac74214bbefdbf9ec2d1b2c2bb249ba70ce53 Mon Sep 17 00:00:00 2001 From: Ben Whittaker Date: Wed, 15 Sep 2021 19:53:35 -0400 Subject: [PATCH 4/4] Optimization --- modules/Layout.js | 18 ++++++++---------- modules/Layout.min.js | 18 ++++++++---------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/modules/Layout.js b/modules/Layout.js index e0d15f8e5..ee5985da9 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -248,13 +248,11 @@ function prepareLazyRender(l, rectsToClear, drawList, rects, bgCol) { // Hash the layoutObject without including its children let c = l.c; delete l.c; - let hash = E.CRC32(E.toJS(l)); + let hash = "H"+E.CRC32(E.toJS(l)); // String keys maintain insertion order if (c) l.c = c; - let i = rectsToClear.findIndex(r => r.h == hash); - if (i != -1) rectsToClear.splice(i, 1); - else { - rects.push({h: hash, bg: bgCol, x1: l.x, y1: l.y, x2: l.x+l.w-1, y2: l.y+l.h-1}); + if (!delete rectsToClear[hash]) { + rects[hash] = {bg: bgCol, r: [l.x,l.y,l.x+l.w-1,l.y+l.h-1]}; if (drawList) { drawList.push(l); drawList = null; // Prevent children from being redundantly added to the drawList @@ -269,13 +267,13 @@ Layout.prototype.render = function (l) { if (!l) l = this._l; if (this.lazy) { - if (!this.rects) this.rects = []; - let rectsToClear = this.rects.slice(); + if (!this.rects) this.rects = {}; + let rectsToClear = this.rects.clone(); let drawList = []; prepareLazyRender(l, rectsToClear, drawList, this.rects, g.getBgColor()); - this.rects = this.rects.filter(r1 => !rectsToClear.some(r2 => r1.h == r2.h)); - rectsToClear.reverse(); // Rects are cleared in reverse order so that the original bg color is restored - for (let r of rectsToClear) g.setBgColor(r.bg).clearRect(r.x1, r.y1, r.x2, r.y2); + for (let h in rectsToClear) delete this.rects[h]; + let clearList = Object.keys(rectsToClear).map(k=>rectsToClear[k]).reverse(); // Rects are cleared in reverse order so that the original bg color is restored + for (let r of clearList) g.setBgColor(r.bg).clearRect.apply(g, r.r); drawList.forEach(render); } else { diff --git a/modules/Layout.min.js b/modules/Layout.min.js index e0d15f8e5..ee5985da9 100644 --- a/modules/Layout.min.js +++ b/modules/Layout.min.js @@ -248,13 +248,11 @@ function prepareLazyRender(l, rectsToClear, drawList, rects, bgCol) { // Hash the layoutObject without including its children let c = l.c; delete l.c; - let hash = E.CRC32(E.toJS(l)); + let hash = "H"+E.CRC32(E.toJS(l)); // String keys maintain insertion order if (c) l.c = c; - let i = rectsToClear.findIndex(r => r.h == hash); - if (i != -1) rectsToClear.splice(i, 1); - else { - rects.push({h: hash, bg: bgCol, x1: l.x, y1: l.y, x2: l.x+l.w-1, y2: l.y+l.h-1}); + if (!delete rectsToClear[hash]) { + rects[hash] = {bg: bgCol, r: [l.x,l.y,l.x+l.w-1,l.y+l.h-1]}; if (drawList) { drawList.push(l); drawList = null; // Prevent children from being redundantly added to the drawList @@ -269,13 +267,13 @@ Layout.prototype.render = function (l) { if (!l) l = this._l; if (this.lazy) { - if (!this.rects) this.rects = []; - let rectsToClear = this.rects.slice(); + if (!this.rects) this.rects = {}; + let rectsToClear = this.rects.clone(); let drawList = []; prepareLazyRender(l, rectsToClear, drawList, this.rects, g.getBgColor()); - this.rects = this.rects.filter(r1 => !rectsToClear.some(r2 => r1.h == r2.h)); - rectsToClear.reverse(); // Rects are cleared in reverse order so that the original bg color is restored - for (let r of rectsToClear) g.setBgColor(r.bg).clearRect(r.x1, r.y1, r.x2, r.y2); + for (let h in rectsToClear) delete this.rects[h]; + let clearList = Object.keys(rectsToClear).map(k=>rectsToClear[k]).reverse(); // Rects are cleared in reverse order so that the original bg color is restored + for (let r of clearList) g.setBgColor(r.bg).clearRect.apply(g, r.r); drawList.forEach(render); } else {