blob: 15be904bf7bc791ad709ba2fee38ae4818b43206 [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 */
33const SCROLLING_CANVAS_OVERDRAW_FACTOR = 2;
34
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 Roy1f658fe2018-09-11 08:38:17 -040060 // attrs received in the most recent mithril redraw.
61 private attrs?: Attrs;
62
63 private canvasOverdrawFactor: number;
64 private ctx?: CanvasRenderingContext2D;
65
66 private onResize: () => void = () => {};
67 private parentOnScroll: () => void = () => {};
68 private canvasRedrawer: () => void;
69
70 constructor(vnode: m.CVnode<Attrs>) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -040071 this.canvasOverdrawFactor =
72 vnode.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040073 this.canvasRedrawer = () => this.redrawCanvas();
Deepanjan Royabd79aa2018-08-28 07:29:15 -040074 globals.rafScheduler.addRedrawCallback(this.canvasRedrawer);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -040075 perfDisplay.addContainer(this);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040076 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -040077
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040078 oncreate(vnodeDom: m.CVnodeDOM<Attrs>) {
79 const attrs = vnodeDom.attrs;
Deepanjan Royabd79aa2018-08-28 07:29:15 -040080 // Save the canvas context in the state.
81 const canvas =
82 vnodeDom.dom.querySelector('.main-canvas') as HTMLCanvasElement;
83 const ctx = canvas.getContext('2d');
84 if (!ctx) {
85 throw Error('Cannot create canvas context');
86 }
87 this.ctx = ctx;
88
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040089 const clientRect =
90 assertExists(vnodeDom.dom.parentElement).getBoundingClientRect();
91 this.parentWidth = clientRect.width;
92 this.parentHeight = clientRect.height;
93
94 this.updatePanelHeightsFromDom(vnodeDom);
95 (vnodeDom.dom as HTMLElement).style.height = `${this.totalPanelHeight}px`;
96
97 this.canvasHeight = this.getCanvasHeight(attrs.doesScroll);
98 this.updateCanvasDimensions(vnodeDom);
Deepanjan Royabd79aa2018-08-28 07:29:15 -040099
100 // Save the resize handler in the state so we can remove it later.
101 // TODO: Encapsulate resize handling better.
102 this.onResize = () => {
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400103 const clientRect =
104 assertExists(vnodeDom.dom.parentElement).getBoundingClientRect();
105 this.parentWidth = clientRect.width;
106 this.parentHeight = clientRect.height;
107 this.canvasHeight = this.getCanvasHeight(attrs.doesScroll);
108 this.updateCanvasDimensions(vnodeDom);
Deepanjan Roy112ff6a2018-09-10 08:31:43 -0400109 globals.rafScheduler.scheduleFullRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400110 };
111
112 // Once ResizeObservers are out, we can stop accessing the window here.
113 window.addEventListener('resize', this.onResize);
114
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400115 // TODO(dproy): Handle change in doesScroll attribute.
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400116 if (vnodeDom.attrs.doesScroll) {
117 this.parentOnScroll = () => {
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400118 this.scrollTop = vnodeDom.dom.parentElement!.scrollTop;
119 this.repositionCanvas(vnodeDom);
Deepanjan Royf190cb22018-08-28 10:43:07 -0400120 globals.rafScheduler.scheduleRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400121 };
122 vnodeDom.dom.parentElement!.addEventListener(
123 'scroll', this.parentOnScroll, {passive: true});
124 }
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400125 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400126
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400127 onremove({attrs, dom}: m.CVnodeDOM<Attrs>) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400128 window.removeEventListener('resize', this.onResize);
129 globals.rafScheduler.removeRedrawCallback(this.canvasRedrawer);
130 if (attrs.doesScroll) {
131 dom.parentElement!.removeEventListener('scroll', this.parentOnScroll);
132 }
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400133 perfDisplay.removeContainer(this);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400134 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400135
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400136 view({attrs}: m.CVnode<Attrs>) {
137 // We receive a new vnode object with new attrs on every mithril redraw. We
138 // store the latest attrs so redrawCanvas can use it.
139 this.attrs = attrs;
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400140 const renderPanel = (panel: m.Vnode) => perfDebug() ?
141 m('.panel', panel, m('.debug-panel-border')) :
142 m('.panel', panel);
143
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400144 return m(
145 '.scroll-limiter',
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400146 m('canvas.main-canvas'),
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400147 attrs.panels.map(renderPanel));
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400148 }
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400149
150 onupdate(vnodeDom: m.CVnodeDOM<Attrs>) {
151 this.repositionCanvas(vnodeDom);
152
153 if (this.updatePanelHeightsFromDom(vnodeDom)) {
154 (vnodeDom.dom as HTMLElement).style.height = `${this.totalPanelHeight}px`;
155 }
156
157 // In non-scrolling case, canvas height can change if panel heights changed.
158 const canvasHeight = this.getCanvasHeight(vnodeDom.attrs.doesScroll);
159 if (this.canvasHeight !== canvasHeight) {
160 this.canvasHeight = canvasHeight;
161 this.updateCanvasDimensions(vnodeDom);
162 }
163 }
164
165 private updateCanvasDimensions(vnodeDom: m.CVnodeDOM<Attrs>) {
166 const canvas =
167 assertExists(vnodeDom.dom.querySelector('canvas.main-canvas')) as
168 HTMLCanvasElement;
169 const ctx = assertExists(this.ctx);
170 canvas.style.height = `${this.canvasHeight}px`;
171 const dpr = window.devicePixelRatio;
172 ctx.canvas.width = this.parentWidth * dpr;
173 ctx.canvas.height = this.canvasHeight * dpr;
174 ctx.scale(dpr, dpr);
175 }
176
177 private updatePanelHeightsFromDom(vnodeDom: m.CVnodeDOM<Attrs>): boolean {
178 const prevHeight = this.totalPanelHeight;
179 this.panelHeights = [];
180 this.totalPanelHeight = 0;
181
182 const panels = vnodeDom.dom.querySelectorAll('.panel');
183 assertTrue(panels.length === vnodeDom.attrs.panels.length);
184 for (let i = 0; i < panels.length; i++) {
185 const height = panels[i].getBoundingClientRect().height;
186 this.panelHeights[i] = height;
187 this.totalPanelHeight += height;
188 }
189
190 return this.totalPanelHeight !== prevHeight;
191 }
192
193 private getCanvasHeight(doesScroll: boolean) {
194 return doesScroll ? this.parentHeight * this.canvasOverdrawFactor :
195 this.totalPanelHeight;
196 }
197
198 private repositionCanvas(vnodeDom: m.CVnodeDOM<Attrs>) {
199 const canvas =
200 assertExists(vnodeDom.dom.querySelector('canvas.main-canvas')) as
201 HTMLCanvasElement;
202 const canvasYStart = this.scrollTop - this.getCanvasOverdrawHeightPerSide();
203 canvas.style.transform = `translateY(${canvasYStart}px)`;
204 }
205
206 private overlapsCanvas(yStart: number, yEnd: number) {
207 return yEnd > 0 && yStart < this.canvasHeight;
208 }
209
210 private redrawCanvas() {
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400211 const redrawStart = debugNow();
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400212 if (!this.ctx) return;
213 this.ctx.clearRect(0, 0, this.parentWidth, this.canvasHeight);
214 const canvasYStart = this.scrollTop - this.getCanvasOverdrawHeightPerSide();
215
216 let panelYStart = 0;
217 const panels = assertExists(this.attrs).panels;
218 assertTrue(panels.length === this.panelHeights.length);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400219 let totalOnCanvas = 0;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400220 for (let i = 0; i < panels.length; i++) {
221 const panel = panels[i];
222 const panelHeight = this.panelHeights[i];
223 const yStartOnCanvas = panelYStart - canvasYStart;
224
225 if (!this.overlapsCanvas(yStartOnCanvas, yStartOnCanvas + panelHeight)) {
226 panelYStart += panelHeight;
227 continue;
228 }
229
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400230 totalOnCanvas++;
231
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400232 if (!isPanelVNode(panel)) {
233 throw Error('Vnode passed to panel container is not a panel');
234 }
235
236 this.ctx.save();
237 this.ctx.translate(0, yStartOnCanvas);
238 const clipRect = new Path2D();
239 const size = {width: this.parentWidth, height: panelHeight};
240 clipRect.rect(0, 0, size.width, size.height);
241 this.ctx.clip(clipRect);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400242 const beforeRender = debugNow();
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400243 panel.state.renderCanvas(this.ctx, size, panel);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400244 this.updatePanelStats(
245 i, panel.state, debugNow() - beforeRender, this.ctx, size);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400246 this.ctx.restore();
247 panelYStart += panelHeight;
248 }
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400249 const redrawDur = debugNow() - redrawStart;
250 this.updatePerfStats(redrawDur, panels.length, totalOnCanvas);
251 }
252
253 private updatePanelStats(
254 panelIndex: number, panel: Panel, renderTime: number,
255 ctx: CanvasRenderingContext2D, size: PanelSize) {
256 if (!perfDebug()) return;
257 let renderStats = this.panelPerfStats.get(panel);
258 if (renderStats === undefined) {
259 renderStats = new RunningStatistics();
260 this.panelPerfStats.set(panel, renderStats);
261 }
262 renderStats.addValue(renderTime);
263
264 const statW = 300;
265 ctx.fillStyle = 'hsl(97, 100%, 96%)';
266 ctx.fillRect(size.width - statW, size.height - 20, statW, 20);
267 ctx.fillStyle = 'hsla(122, 77%, 22%)';
268 const statStr = `Panel ${panelIndex + 1} | ` + runningStatStr(renderStats);
269 ctx.fillText(statStr, size.width - statW, size.height - 10);
270 }
271
272 private updatePerfStats(
273 renderTime: number, totalPanels: number, panelsOnCanvas: number) {
274 if (!perfDebug()) return;
275 this.perfStats.renderStats.addValue(renderTime);
276 this.perfStats.totalPanels = totalPanels;
277 this.perfStats.panelsOnCanvas = panelsOnCanvas;
278 }
279
280 renderPerfStats(index: number) {
281 assertTrue(perfDebug());
282 return [m(
283 'section',
284 m('div', `Panel Container ${index + 1}`),
285 m('div',
286 `${this.perfStats.totalPanels} panels, ` +
287 `${this.perfStats.panelsOnCanvas} on canvas.`),
288 m('div', runningStatStr(this.perfStats.renderStats)), )];
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400289 }
290
291 private getCanvasOverdrawHeightPerSide() {
292 const overdrawHeight = (this.canvasOverdrawFactor - 1) * this.parentHeight;
293 return overdrawHeight / 2;
294 }
295}