blob: d040eea82e89fd7c09c52f6ef019f5f0d2060cae [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';
Isabelle Taylorf41b7c72019-07-24 13:55:43 +010028import {TRACK_SHELL_WIDTH} from './track_constants';
Deepanjan Royabd79aa2018-08-28 07:29:15 -040029
30/**
31 * If the panel container scrolls, the backing canvas height is
32 * SCROLLING_CANVAS_OVERDRAW_FACTOR * parent container height.
33 */
Hector Dearman0d825912019-02-15 10:33:37 +000034const SCROLLING_CANVAS_OVERDRAW_FACTOR = 1.2;
Deepanjan Royabd79aa2018-08-28 07:29:15 -040035
Deepanjan Roy4fa0ecc2018-10-03 16:10:46 -040036// We need any here so we can accept vnodes with arbitrary attrs.
37// tslint:disable-next-line:no-any
38export type AnyAttrsVnode = m.Vnode<any, {}>;
39
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040040interface Attrs {
Deepanjan Roy4fa0ecc2018-10-03 16:10:46 -040041 panels: AnyAttrsVnode[];
Deepanjan Royabd79aa2018-08-28 07:29:15 -040042 doesScroll: boolean;
Isabelle Taylorf41b7c72019-07-24 13:55:43 +010043 kind: 'TRACKS'|'OVERVIEW'|'DETAILS';
Deepanjan Royabd79aa2018-08-28 07:29:15 -040044}
45
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040046export class PanelContainer implements m.ClassComponent<Attrs> {
47 // These values are updated with proper values in oncreate.
48 private parentWidth = 0;
49 private parentHeight = 0;
50 private scrollTop = 0;
51 private panelHeights: number[] = [];
52 private totalPanelHeight = 0;
53 private canvasHeight = 0;
Deepanjan Royabd79aa2018-08-28 07:29:15 -040054
Deepanjan Roy9a906ed2018-09-20 11:06:00 -040055 private panelPerfStats = new WeakMap<Panel, RunningStatistics>();
56 private perfStats = {
57 totalPanels: 0,
58 panelsOnCanvas: 0,
59 renderStats: new RunningStatistics(10),
60 };
61
Deepanjan Roy1a225c22018-10-04 07:33:02 -040062 // Attrs received in the most recent mithril redraw. We receive a new vnode
63 // with new attrs on every redraw, and we cache it here so that resize
64 // listeners and canvas redraw callbacks can access it.
65 private attrs: Attrs;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040066
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040067 private ctx?: CanvasRenderingContext2D;
68
69 private onResize: () => void = () => {};
70 private parentOnScroll: () => void = () => {};
71 private canvasRedrawer: () => void;
72
Deepanjan Roy1a225c22018-10-04 07:33:02 -040073 get canvasOverdrawFactor() {
74 return this.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
75 }
76
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040077 constructor(vnode: m.CVnode<Attrs>) {
Deepanjan Roy1a225c22018-10-04 07:33:02 -040078 this.attrs = vnode.attrs;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040079 this.canvasRedrawer = () => this.redrawCanvas();
Deepanjan Royabd79aa2018-08-28 07:29:15 -040080 globals.rafScheduler.addRedrawCallback(this.canvasRedrawer);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -040081 perfDisplay.addContainer(this);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040082 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -040083
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040084 oncreate(vnodeDom: m.CVnodeDOM<Attrs>) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -040085 // Save the canvas context in the state.
86 const canvas =
87 vnodeDom.dom.querySelector('.main-canvas') as HTMLCanvasElement;
88 const ctx = canvas.getContext('2d');
89 if (!ctx) {
90 throw Error('Cannot create canvas context');
91 }
92 this.ctx = ctx;
93
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040094 const clientRect =
95 assertExists(vnodeDom.dom.parentElement).getBoundingClientRect();
96 this.parentWidth = clientRect.width;
97 this.parentHeight = clientRect.height;
98
Deepanjan Roy1a225c22018-10-04 07:33:02 -040099 this.readPanelHeightsFromDom(vnodeDom.dom);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400100
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400101 this.updateCanvasDimensions();
102 this.repositionCanvas();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400103
104 // Save the resize handler in the state so we can remove it later.
105 // TODO: Encapsulate resize handling better.
106 this.onResize = () => {
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400107 this.readParentSizeFromDom(vnodeDom.dom);
108 this.updateCanvasDimensions();
109 this.repositionCanvas();
Deepanjan Roy112ff6a2018-09-10 08:31:43 -0400110 globals.rafScheduler.scheduleFullRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400111 };
112
113 // Once ResizeObservers are out, we can stop accessing the window here.
114 window.addEventListener('resize', this.onResize);
115
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400116 // TODO(dproy): Handle change in doesScroll attribute.
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400117 if (this.attrs.doesScroll) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400118 this.parentOnScroll = () => {
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400119 this.scrollTop = assertExists(vnodeDom.dom.parentElement).scrollTop;
120 this.repositionCanvas();
Deepanjan Royf190cb22018-08-28 10:43:07 -0400121 globals.rafScheduler.scheduleRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400122 };
123 vnodeDom.dom.parentElement!.addEventListener(
124 'scroll', this.parentOnScroll, {passive: true});
125 }
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400126 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400127
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400128 onremove({attrs, dom}: m.CVnodeDOM<Attrs>) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400129 window.removeEventListener('resize', this.onResize);
130 globals.rafScheduler.removeRedrawCallback(this.canvasRedrawer);
131 if (attrs.doesScroll) {
132 dom.parentElement!.removeEventListener('scroll', this.parentOnScroll);
133 }
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400134 perfDisplay.removeContainer(this);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400135 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400136
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400137 view({attrs}: m.CVnode<Attrs>) {
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400138 this.attrs = attrs;
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400139 const renderPanel = (panel: m.Vnode) => perfDebug() ?
140 m('.panel', panel, m('.debug-panel-border')) :
Isabelle Taylor1acfa2b2019-05-17 17:26:40 +0100141 m('.panel', {key: panel.key}, panel);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400142
Isabelle Taylorf4e0f682019-11-20 10:47:49 +0000143 return [
144 m(
145 '.scroll-limiter',
146 m('canvas.main-canvas'),
147 ),
148 m('.panels', attrs.panels.map(renderPanel))
149 ];
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400150 }
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400151
152 onupdate(vnodeDom: m.CVnodeDOM<Attrs>) {
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400153 const totalPanelHeightChanged = this.readPanelHeightsFromDom(vnodeDom.dom);
154 const parentSizeChanged = this.readParentSizeFromDom(vnodeDom.dom);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400155
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400156 const canvasSizeShouldChange =
Isabelle Taylorf41b7c72019-07-24 13:55:43 +0100157 parentSizeChanged || !this.attrs.doesScroll && totalPanelHeightChanged;
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400158 if (canvasSizeShouldChange) {
159 this.updateCanvasDimensions();
160 this.repositionCanvas();
Isabelle Taylorf41b7c72019-07-24 13:55:43 +0100161 if (this.attrs.kind === 'TRACKS') {
162 globals.frontendLocalState.timeScale.setLimitsPx(
163 0, this.parentWidth - TRACK_SHELL_WIDTH);
164 }
165 this.redrawCanvas();
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400166 }
167 }
168
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400169 private updateCanvasDimensions() {
Hector Dearmanafa51b22018-12-11 16:38:30 +0000170 this.canvasHeight = Math.floor(
171 this.attrs.doesScroll ? this.parentHeight * this.canvasOverdrawFactor :
172 this.totalPanelHeight);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400173 const ctx = assertExists(this.ctx);
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400174 const canvas = assertExists(ctx.canvas);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400175 canvas.style.height = `${this.canvasHeight}px`;
176 const dpr = window.devicePixelRatio;
Isabelle Taylor2b74c8c2019-09-12 17:35:43 +0100177 // On non-MacOS if there is a solid scroll bar it can cover important
178 // pixels, reduce the size of the canvas so it doesn't overlap with
179 // the scroll bar.
180 ctx.canvas.width =
181 (this.parentWidth - globals.frontendLocalState.getScrollbarWidth()) *
182 dpr;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400183 ctx.canvas.height = this.canvasHeight * dpr;
184 ctx.scale(dpr, dpr);
185 }
186
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400187 private repositionCanvas() {
188 const canvas = assertExists(assertExists(this.ctx).canvas);
Hector Dearmanafa51b22018-12-11 16:38:30 +0000189 const canvasYStart =
190 Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400191 canvas.style.transform = `translateY(${canvasYStart}px)`;
192 }
193
194 /**
195 * Reads dimensions of parent node. Returns true if read dimensions are
196 * different from what was cached in the state.
197 */
198 private readParentSizeFromDom(dom: Element): boolean {
199 const oldWidth = this.parentWidth;
200 const oldHeight = this.parentHeight;
201 const clientRect = assertExists(dom.parentElement).getBoundingClientRect();
202 this.parentWidth = clientRect.width;
203 this.parentHeight = clientRect.height;
204 return this.parentHeight !== oldHeight || this.parentWidth !== oldWidth;
205 }
206
207 /**
208 * Reads dimensions of panels. Returns true if total panel height is different
209 * from what was cached in state.
210 */
211 private readPanelHeightsFromDom(dom: Element): boolean {
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400212 const prevHeight = this.totalPanelHeight;
213 this.panelHeights = [];
214 this.totalPanelHeight = 0;
215
Isabelle Taylorf4e0f682019-11-20 10:47:49 +0000216 const panels = dom.parentElement!.querySelectorAll('.panel');
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400217 assertTrue(panels.length === this.attrs.panels.length);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400218 for (let i = 0; i < panels.length; i++) {
219 const height = panels[i].getBoundingClientRect().height;
220 this.panelHeights[i] = height;
221 this.totalPanelHeight += height;
222 }
223
224 return this.totalPanelHeight !== prevHeight;
225 }
226
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400227 private overlapsCanvas(yStart: number, yEnd: number) {
228 return yEnd > 0 && yStart < this.canvasHeight;
229 }
230
231 private redrawCanvas() {
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400232 const redrawStart = debugNow();
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400233 if (!this.ctx) return;
234 this.ctx.clearRect(0, 0, this.parentWidth, this.canvasHeight);
Hector Dearmanf74cf222018-10-31 16:22:29 +0000235 const canvasYStart =
Hector Dearmanafa51b22018-12-11 16:38:30 +0000236 Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400237
238 let panelYStart = 0;
239 const panels = assertExists(this.attrs).panels;
240 assertTrue(panels.length === this.panelHeights.length);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400241 let totalOnCanvas = 0;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400242 for (let i = 0; i < panels.length; i++) {
243 const panel = panels[i];
244 const panelHeight = this.panelHeights[i];
245 const yStartOnCanvas = panelYStart - canvasYStart;
246
247 if (!this.overlapsCanvas(yStartOnCanvas, yStartOnCanvas + panelHeight)) {
248 panelYStart += panelHeight;
249 continue;
250 }
251
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400252 totalOnCanvas++;
253
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400254 if (!isPanelVNode(panel)) {
255 throw Error('Vnode passed to panel container is not a panel');
256 }
257
258 this.ctx.save();
259 this.ctx.translate(0, yStartOnCanvas);
260 const clipRect = new Path2D();
261 const size = {width: this.parentWidth, height: panelHeight};
262 clipRect.rect(0, 0, size.width, size.height);
263 this.ctx.clip(clipRect);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400264 const beforeRender = debugNow();
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400265 panel.state.renderCanvas(this.ctx, size, panel);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400266 this.updatePanelStats(
267 i, panel.state, debugNow() - beforeRender, this.ctx, size);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400268 this.ctx.restore();
269 panelYStart += panelHeight;
270 }
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400271 const redrawDur = debugNow() - redrawStart;
272 this.updatePerfStats(redrawDur, panels.length, totalOnCanvas);
273 }
274
275 private updatePanelStats(
276 panelIndex: number, panel: Panel, renderTime: number,
277 ctx: CanvasRenderingContext2D, size: PanelSize) {
278 if (!perfDebug()) return;
279 let renderStats = this.panelPerfStats.get(panel);
280 if (renderStats === undefined) {
281 renderStats = new RunningStatistics();
282 this.panelPerfStats.set(panel, renderStats);
283 }
284 renderStats.addValue(renderTime);
285
286 const statW = 300;
287 ctx.fillStyle = 'hsl(97, 100%, 96%)';
288 ctx.fillRect(size.width - statW, size.height - 20, statW, 20);
289 ctx.fillStyle = 'hsla(122, 77%, 22%)';
290 const statStr = `Panel ${panelIndex + 1} | ` + runningStatStr(renderStats);
291 ctx.fillText(statStr, size.width - statW, size.height - 10);
292 }
293
294 private updatePerfStats(
295 renderTime: number, totalPanels: number, panelsOnCanvas: number) {
296 if (!perfDebug()) return;
297 this.perfStats.renderStats.addValue(renderTime);
298 this.perfStats.totalPanels = totalPanels;
299 this.perfStats.panelsOnCanvas = panelsOnCanvas;
300 }
301
302 renderPerfStats(index: number) {
303 assertTrue(perfDebug());
304 return [m(
305 'section',
306 m('div', `Panel Container ${index + 1}`),
307 m('div',
308 `${this.perfStats.totalPanels} panels, ` +
309 `${this.perfStats.panelsOnCanvas} on canvas.`),
310 m('div', runningStatStr(this.perfStats.renderStats)), )];
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400311 }
312
313 private getCanvasOverdrawHeightPerSide() {
314 const overdrawHeight = (this.canvasOverdrawFactor - 1) * this.parentHeight;
315 return overdrawHeight / 2;
316 }
317}