• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// 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
17import {assertExists, assertTrue} from '../base/logging';
18
19import {globals} from './globals';
20import {isPanelVNode, Panel, PanelSize} from './panel';
21import {
22  debugNow,
23  perfDebug,
24  perfDisplay,
25  RunningStatistics,
26  runningStatStr
27} from './perf';
28
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 = 1.2;
34
35// 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
39interface Attrs {
40  panels: AnyAttrsVnode[];
41  doesScroll: boolean;
42}
43
44export 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;
52
53  private panelPerfStats = new WeakMap<Panel, RunningStatistics>();
54  private perfStats = {
55    totalPanels: 0,
56    panelsOnCanvas: 0,
57    renderStats: new RunningStatistics(10),
58  };
59
60  // 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;
64
65  private ctx?: CanvasRenderingContext2D;
66
67  private onResize: () => void = () => {};
68  private parentOnScroll: () => void = () => {};
69  private canvasRedrawer: () => void;
70
71  get canvasOverdrawFactor() {
72    return this.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
73  }
74
75  constructor(vnode: m.CVnode<Attrs>) {
76    this.attrs = vnode.attrs;
77    this.canvasRedrawer = () => this.redrawCanvas();
78    globals.rafScheduler.addRedrawCallback(this.canvasRedrawer);
79    perfDisplay.addContainer(this);
80  }
81
82  oncreate(vnodeDom: m.CVnodeDOM<Attrs>) {
83    // 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
92    const clientRect =
93        assertExists(vnodeDom.dom.parentElement).getBoundingClientRect();
94    this.parentWidth = clientRect.width;
95    this.parentHeight = clientRect.height;
96
97    this.readPanelHeightsFromDom(vnodeDom.dom);
98
99    this.updateCanvasDimensions();
100    this.repositionCanvas();
101
102    // Save the resize handler in the state so we can remove it later.
103    // TODO: Encapsulate resize handling better.
104    this.onResize = () => {
105      this.readParentSizeFromDom(vnodeDom.dom);
106      this.updateCanvasDimensions();
107      this.repositionCanvas();
108      globals.rafScheduler.scheduleFullRedraw();
109    };
110
111    // Once ResizeObservers are out, we can stop accessing the window here.
112    window.addEventListener('resize', this.onResize);
113
114    // TODO(dproy): Handle change in doesScroll attribute.
115    if (this.attrs.doesScroll) {
116      this.parentOnScroll = () => {
117        this.scrollTop = assertExists(vnodeDom.dom.parentElement).scrollTop;
118        this.repositionCanvas();
119        globals.rafScheduler.scheduleRedraw();
120      };
121      vnodeDom.dom.parentElement!.addEventListener(
122          'scroll', this.parentOnScroll, {passive: true});
123    }
124  }
125
126  onremove({attrs, dom}: m.CVnodeDOM<Attrs>) {
127    window.removeEventListener('resize', this.onResize);
128    globals.rafScheduler.removeRedrawCallback(this.canvasRedrawer);
129    if (attrs.doesScroll) {
130      dom.parentElement!.removeEventListener('scroll', this.parentOnScroll);
131    }
132    perfDisplay.removeContainer(this);
133  }
134
135  view({attrs}: m.CVnode<Attrs>) {
136    this.attrs = attrs;
137    const renderPanel = (panel: m.Vnode) => perfDebug() ?
138        m('.panel', panel, m('.debug-panel-border')) :
139        m('.panel', panel);
140
141    return m(
142        '.scroll-limiter',
143        m('canvas.main-canvas'),
144        attrs.panels.map(renderPanel));
145  }
146
147  onupdate(vnodeDom: m.CVnodeDOM<Attrs>) {
148    const totalPanelHeightChanged = this.readPanelHeightsFromDom(vnodeDom.dom);
149    const parentSizeChanged = this.readParentSizeFromDom(vnodeDom.dom);
150
151    const canvasSizeShouldChange =
152        this.attrs.doesScroll ? parentSizeChanged : totalPanelHeightChanged;
153    if (canvasSizeShouldChange) {
154      this.updateCanvasDimensions();
155      this.repositionCanvas();
156    }
157  }
158
159  private updateCanvasDimensions() {
160    this.canvasHeight = Math.floor(
161        this.attrs.doesScroll ? this.parentHeight * this.canvasOverdrawFactor :
162                                this.totalPanelHeight);
163    const ctx = assertExists(this.ctx);
164    const canvas = assertExists(ctx.canvas);
165    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
172  private repositionCanvas() {
173    const canvas = assertExists(assertExists(this.ctx).canvas);
174    const canvasYStart =
175        Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
176    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 {
197    const prevHeight = this.totalPanelHeight;
198    this.panelHeights = [];
199    this.totalPanelHeight = 0;
200
201    const panels = dom.querySelectorAll('.panel');
202    assertTrue(panels.length === this.attrs.panels.length);
203    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
212  private overlapsCanvas(yStart: number, yEnd: number) {
213    return yEnd > 0 && yStart < this.canvasHeight;
214  }
215
216  private redrawCanvas() {
217    const redrawStart = debugNow();
218    if (!this.ctx) return;
219    this.ctx.clearRect(0, 0, this.parentWidth, this.canvasHeight);
220    const canvasYStart =
221        Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
222
223    let panelYStart = 0;
224    const panels = assertExists(this.attrs).panels;
225    assertTrue(panels.length === this.panelHeights.length);
226    let totalOnCanvas = 0;
227    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
237      totalOnCanvas++;
238
239      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);
249      const beforeRender = debugNow();
250      panel.state.renderCanvas(this.ctx, size, panel);
251      this.updatePanelStats(
252          i, panel.state, debugNow() - beforeRender, this.ctx, size);
253      this.ctx.restore();
254      panelYStart += panelHeight;
255    }
256    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)), )];
296  }
297
298  private getCanvasOverdrawHeightPerSide() {
299    const overdrawHeight = (this.canvasOverdrawFactor - 1) * this.parentHeight;
300    return overdrawHeight / 2;
301  }
302}
303