blob: a57f0688b95b4fa2e5aeb341e2d02eabe265f0ab [file] [log] [blame]
Deepanjan Royabd79aa2018-08-28 07:29:15 -04001// Copyright (C) 2018 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import * as m from 'mithril';
16
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040017import {assertExists, assertTrue} from '../base/logging';
Deepanjan Royabd79aa2018-08-28 07:29:15 -040018
19import {globals} from './globals';
Deepanjan Roy9a906ed2018-09-20 11:06:00 -040020import {isPanelVNode, Panel, PanelSize} from './panel';
21import {
22 debugNow,
23 perfDebug,
24 perfDisplay,
25 RunningStatistics,
26 runningStatStr
27} from './perf';
Deepanjan Royabd79aa2018-08-28 07:29:15 -040028
29/**
30 * If the panel container scrolls, the backing canvas height is
31 * SCROLLING_CANVAS_OVERDRAW_FACTOR * parent container height.
32 */
Hector Dearman0d825912019-02-15 10:33:37 +000033const SCROLLING_CANVAS_OVERDRAW_FACTOR = 1.2;
Deepanjan Royabd79aa2018-08-28 07:29:15 -040034
Deepanjan Roy4fa0ecc2018-10-03 16:10:46 -040035// We need any here so we can accept vnodes with arbitrary attrs.
36// tslint:disable-next-line:no-any
37export type AnyAttrsVnode = m.Vnode<any, {}>;
38
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040039interface Attrs {
Deepanjan Roy4fa0ecc2018-10-03 16:10:46 -040040 panels: AnyAttrsVnode[];
Deepanjan Royabd79aa2018-08-28 07:29:15 -040041 doesScroll: boolean;
42}
43
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040044export class PanelContainer implements m.ClassComponent<Attrs> {
45 // These values are updated with proper values in oncreate.
46 private parentWidth = 0;
47 private parentHeight = 0;
48 private scrollTop = 0;
49 private panelHeights: number[] = [];
50 private totalPanelHeight = 0;
51 private canvasHeight = 0;
Deepanjan Royabd79aa2018-08-28 07:29:15 -040052
Deepanjan Roy9a906ed2018-09-20 11:06:00 -040053 private panelPerfStats = new WeakMap<Panel, RunningStatistics>();
54 private perfStats = {
55 totalPanels: 0,
56 panelsOnCanvas: 0,
57 renderStats: new RunningStatistics(10),
58 };
59
Deepanjan Roy1a225c22018-10-04 07:33:02 -040060 // Attrs received in the most recent mithril redraw. We receive a new vnode
61 // with new attrs on every redraw, and we cache it here so that resize
62 // listeners and canvas redraw callbacks can access it.
63 private attrs: Attrs;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040064
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040065 private ctx?: CanvasRenderingContext2D;
66
67 private onResize: () => void = () => {};
68 private parentOnScroll: () => void = () => {};
69 private canvasRedrawer: () => void;
70
Deepanjan Roy1a225c22018-10-04 07:33:02 -040071 get canvasOverdrawFactor() {
72 return this.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
73 }
74
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040075 constructor(vnode: m.CVnode<Attrs>) {
Deepanjan Roy1a225c22018-10-04 07:33:02 -040076 this.attrs = vnode.attrs;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040077 this.canvasRedrawer = () => this.redrawCanvas();
Deepanjan Royabd79aa2018-08-28 07:29:15 -040078 globals.rafScheduler.addRedrawCallback(this.canvasRedrawer);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -040079 perfDisplay.addContainer(this);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040080 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -040081
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040082 oncreate(vnodeDom: m.CVnodeDOM<Attrs>) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -040083 // Save the canvas context in the state.
84 const canvas =
85 vnodeDom.dom.querySelector('.main-canvas') as HTMLCanvasElement;
86 const ctx = canvas.getContext('2d');
87 if (!ctx) {
88 throw Error('Cannot create canvas context');
89 }
90 this.ctx = ctx;
91
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040092 const clientRect =
93 assertExists(vnodeDom.dom.parentElement).getBoundingClientRect();
94 this.parentWidth = clientRect.width;
95 this.parentHeight = clientRect.height;
96
Deepanjan Roy1a225c22018-10-04 07:33:02 -040097 this.readPanelHeightsFromDom(vnodeDom.dom);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040098
Deepanjan Roy1a225c22018-10-04 07:33:02 -040099 this.updateCanvasDimensions();
100 this.repositionCanvas();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400101
102 // Save the resize handler in the state so we can remove it later.
103 // TODO: Encapsulate resize handling better.
104 this.onResize = () => {
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400105 this.readParentSizeFromDom(vnodeDom.dom);
106 this.updateCanvasDimensions();
107 this.repositionCanvas();
Deepanjan Roy112ff6a2018-09-10 08:31:43 -0400108 globals.rafScheduler.scheduleFullRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400109 };
110
111 // Once ResizeObservers are out, we can stop accessing the window here.
112 window.addEventListener('resize', this.onResize);
113
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400114 // TODO(dproy): Handle change in doesScroll attribute.
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400115 if (this.attrs.doesScroll) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400116 this.parentOnScroll = () => {
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400117 this.scrollTop = assertExists(vnodeDom.dom.parentElement).scrollTop;
118 this.repositionCanvas();
Deepanjan Royf190cb22018-08-28 10:43:07 -0400119 globals.rafScheduler.scheduleRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400120 };
121 vnodeDom.dom.parentElement!.addEventListener(
122 'scroll', this.parentOnScroll, {passive: true});
123 }
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400124 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400125
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400126 onremove({attrs, dom}: m.CVnodeDOM<Attrs>) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400127 window.removeEventListener('resize', this.onResize);
128 globals.rafScheduler.removeRedrawCallback(this.canvasRedrawer);
129 if (attrs.doesScroll) {
130 dom.parentElement!.removeEventListener('scroll', this.parentOnScroll);
131 }
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400132 perfDisplay.removeContainer(this);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400133 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400134
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400135 view({attrs}: m.CVnode<Attrs>) {
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400136 this.attrs = attrs;
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400137 const renderPanel = (panel: m.Vnode) => perfDebug() ?
138 m('.panel', panel, m('.debug-panel-border')) :
Isabelle Taylor1acfa2b2019-05-17 17:26:40 +0100139 m('.panel', {key: panel.key}, panel);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400140
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400141 return m(
142 '.scroll-limiter',
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400143 m('canvas.main-canvas'),
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400144 attrs.panels.map(renderPanel));
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400145 }
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400146
147 onupdate(vnodeDom: m.CVnodeDOM<Attrs>) {
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400148 const totalPanelHeightChanged = this.readPanelHeightsFromDom(vnodeDom.dom);
149 const parentSizeChanged = this.readParentSizeFromDom(vnodeDom.dom);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400150
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400151 const canvasSizeShouldChange =
152 this.attrs.doesScroll ? parentSizeChanged : totalPanelHeightChanged;
153 if (canvasSizeShouldChange) {
154 this.updateCanvasDimensions();
155 this.repositionCanvas();
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400156 }
157 }
158
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400159 private updateCanvasDimensions() {
Hector Dearmanafa51b22018-12-11 16:38:30 +0000160 this.canvasHeight = Math.floor(
161 this.attrs.doesScroll ? this.parentHeight * this.canvasOverdrawFactor :
162 this.totalPanelHeight);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400163 const ctx = assertExists(this.ctx);
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400164 const canvas = assertExists(ctx.canvas);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400165 canvas.style.height = `${this.canvasHeight}px`;
166 const dpr = window.devicePixelRatio;
167 ctx.canvas.width = this.parentWidth * dpr;
168 ctx.canvas.height = this.canvasHeight * dpr;
169 ctx.scale(dpr, dpr);
170 }
171
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400172 private repositionCanvas() {
173 const canvas = assertExists(assertExists(this.ctx).canvas);
Hector Dearmanafa51b22018-12-11 16:38:30 +0000174 const canvasYStart =
175 Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400176 canvas.style.transform = `translateY(${canvasYStart}px)`;
177 }
178
179 /**
180 * Reads dimensions of parent node. Returns true if read dimensions are
181 * different from what was cached in the state.
182 */
183 private readParentSizeFromDom(dom: Element): boolean {
184 const oldWidth = this.parentWidth;
185 const oldHeight = this.parentHeight;
186 const clientRect = assertExists(dom.parentElement).getBoundingClientRect();
187 this.parentWidth = clientRect.width;
188 this.parentHeight = clientRect.height;
189 return this.parentHeight !== oldHeight || this.parentWidth !== oldWidth;
190 }
191
192 /**
193 * Reads dimensions of panels. Returns true if total panel height is different
194 * from what was cached in state.
195 */
196 private readPanelHeightsFromDom(dom: Element): boolean {
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400197 const prevHeight = this.totalPanelHeight;
198 this.panelHeights = [];
199 this.totalPanelHeight = 0;
200
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400201 const panels = dom.querySelectorAll('.panel');
202 assertTrue(panels.length === this.attrs.panels.length);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400203 for (let i = 0; i < panels.length; i++) {
204 const height = panels[i].getBoundingClientRect().height;
205 this.panelHeights[i] = height;
206 this.totalPanelHeight += height;
207 }
208
209 return this.totalPanelHeight !== prevHeight;
210 }
211
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400212 private overlapsCanvas(yStart: number, yEnd: number) {
213 return yEnd > 0 && yStart < this.canvasHeight;
214 }
215
216 private redrawCanvas() {
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400217 const redrawStart = debugNow();
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400218 if (!this.ctx) return;
219 this.ctx.clearRect(0, 0, this.parentWidth, this.canvasHeight);
Hector Dearmanf74cf222018-10-31 16:22:29 +0000220 const canvasYStart =
Hector Dearmanafa51b22018-12-11 16:38:30 +0000221 Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400222
223 let panelYStart = 0;
224 const panels = assertExists(this.attrs).panels;
225 assertTrue(panels.length === this.panelHeights.length);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400226 let totalOnCanvas = 0;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400227 for (let i = 0; i < panels.length; i++) {
228 const panel = panels[i];
229 const panelHeight = this.panelHeights[i];
230 const yStartOnCanvas = panelYStart - canvasYStart;
231
232 if (!this.overlapsCanvas(yStartOnCanvas, yStartOnCanvas + panelHeight)) {
233 panelYStart += panelHeight;
234 continue;
235 }
236
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400237 totalOnCanvas++;
238
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400239 if (!isPanelVNode(panel)) {
240 throw Error('Vnode passed to panel container is not a panel');
241 }
242
243 this.ctx.save();
244 this.ctx.translate(0, yStartOnCanvas);
245 const clipRect = new Path2D();
246 const size = {width: this.parentWidth, height: panelHeight};
247 clipRect.rect(0, 0, size.width, size.height);
248 this.ctx.clip(clipRect);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400249 const beforeRender = debugNow();
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400250 panel.state.renderCanvas(this.ctx, size, panel);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400251 this.updatePanelStats(
252 i, panel.state, debugNow() - beforeRender, this.ctx, size);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400253 this.ctx.restore();
254 panelYStart += panelHeight;
255 }
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400256 const redrawDur = debugNow() - redrawStart;
257 this.updatePerfStats(redrawDur, panels.length, totalOnCanvas);
258 }
259
260 private updatePanelStats(
261 panelIndex: number, panel: Panel, renderTime: number,
262 ctx: CanvasRenderingContext2D, size: PanelSize) {
263 if (!perfDebug()) return;
264 let renderStats = this.panelPerfStats.get(panel);
265 if (renderStats === undefined) {
266 renderStats = new RunningStatistics();
267 this.panelPerfStats.set(panel, renderStats);
268 }
269 renderStats.addValue(renderTime);
270
271 const statW = 300;
272 ctx.fillStyle = 'hsl(97, 100%, 96%)';
273 ctx.fillRect(size.width - statW, size.height - 20, statW, 20);
274 ctx.fillStyle = 'hsla(122, 77%, 22%)';
275 const statStr = `Panel ${panelIndex + 1} | ` + runningStatStr(renderStats);
276 ctx.fillText(statStr, size.width - statW, size.height - 10);
277 }
278
279 private updatePerfStats(
280 renderTime: number, totalPanels: number, panelsOnCanvas: number) {
281 if (!perfDebug()) return;
282 this.perfStats.renderStats.addValue(renderTime);
283 this.perfStats.totalPanels = totalPanels;
284 this.perfStats.panelsOnCanvas = panelsOnCanvas;
285 }
286
287 renderPerfStats(index: number) {
288 assertTrue(perfDebug());
289 return [m(
290 'section',
291 m('div', `Panel Container ${index + 1}`),
292 m('div',
293 `${this.perfStats.totalPanels} panels, ` +
294 `${this.perfStats.panelsOnCanvas} on canvas.`),
295 m('div', runningStatStr(this.perfStats.renderStats)), )];
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400296 }
297
298 private getCanvasOverdrawHeightPerSide() {
299 const overdrawHeight = (this.canvasOverdrawFactor - 1) * this.parentHeight;
300 return overdrawHeight / 2;
301 }
302}