blob: a3866bba0f104296e9dbc90b0ae495d54ce60d3c [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 Roy1f658fe2018-09-11 08:38:17 -040020import {isPanelVNode} from './panel';
Deepanjan Royabd79aa2018-08-28 07:29:15 -040021
22/**
23 * If the panel container scrolls, the backing canvas height is
24 * SCROLLING_CANVAS_OVERDRAW_FACTOR * parent container height.
25 */
26const SCROLLING_CANVAS_OVERDRAW_FACTOR = 2;
27
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040028interface Attrs {
29 // Panels with non-empty attrs does not work without any.
30 // tslint:disable-next-line:no-any
31 panels: Array<m.Vnode<any, {}>>;
Deepanjan Royabd79aa2018-08-28 07:29:15 -040032 doesScroll: boolean;
33}
34
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040035export class PanelContainer implements m.ClassComponent<Attrs> {
36 // These values are updated with proper values in oncreate.
37 private parentWidth = 0;
38 private parentHeight = 0;
39 private scrollTop = 0;
40 private panelHeights: number[] = [];
41 private totalPanelHeight = 0;
42 private canvasHeight = 0;
Deepanjan Royabd79aa2018-08-28 07:29:15 -040043
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040044 // attrs received in the most recent mithril redraw.
45 private attrs?: Attrs;
46
47 private canvasOverdrawFactor: number;
48 private ctx?: CanvasRenderingContext2D;
49
50 private onResize: () => void = () => {};
51 private parentOnScroll: () => void = () => {};
52 private canvasRedrawer: () => void;
53
54 constructor(vnode: m.CVnode<Attrs>) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -040055 this.canvasOverdrawFactor =
56 vnode.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040057 this.canvasRedrawer = () => this.redrawCanvas();
Deepanjan Royabd79aa2018-08-28 07:29:15 -040058 globals.rafScheduler.addRedrawCallback(this.canvasRedrawer);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040059 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -040060
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040061 oncreate(vnodeDom: m.CVnodeDOM<Attrs>) {
62 const attrs = vnodeDom.attrs;
Deepanjan Royabd79aa2018-08-28 07:29:15 -040063 // Save the canvas context in the state.
64 const canvas =
65 vnodeDom.dom.querySelector('.main-canvas') as HTMLCanvasElement;
66 const ctx = canvas.getContext('2d');
67 if (!ctx) {
68 throw Error('Cannot create canvas context');
69 }
70 this.ctx = ctx;
71
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040072 const clientRect =
73 assertExists(vnodeDom.dom.parentElement).getBoundingClientRect();
74 this.parentWidth = clientRect.width;
75 this.parentHeight = clientRect.height;
76
77 this.updatePanelHeightsFromDom(vnodeDom);
78 (vnodeDom.dom as HTMLElement).style.height = `${this.totalPanelHeight}px`;
79
80 this.canvasHeight = this.getCanvasHeight(attrs.doesScroll);
81 this.updateCanvasDimensions(vnodeDom);
Deepanjan Royabd79aa2018-08-28 07:29:15 -040082
83 // Save the resize handler in the state so we can remove it later.
84 // TODO: Encapsulate resize handling better.
85 this.onResize = () => {
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040086 const clientRect =
87 assertExists(vnodeDom.dom.parentElement).getBoundingClientRect();
88 this.parentWidth = clientRect.width;
89 this.parentHeight = clientRect.height;
90 this.canvasHeight = this.getCanvasHeight(attrs.doesScroll);
91 this.updateCanvasDimensions(vnodeDom);
Deepanjan Roy112ff6a2018-09-10 08:31:43 -040092 globals.rafScheduler.scheduleFullRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -040093 };
94
95 // Once ResizeObservers are out, we can stop accessing the window here.
96 window.addEventListener('resize', this.onResize);
97
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040098 // TODO(dproy): Handle change in doesScroll attribute.
Deepanjan Royabd79aa2018-08-28 07:29:15 -040099 if (vnodeDom.attrs.doesScroll) {
100 this.parentOnScroll = () => {
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400101 this.scrollTop = vnodeDom.dom.parentElement!.scrollTop;
102 this.repositionCanvas(vnodeDom);
Deepanjan Royf190cb22018-08-28 10:43:07 -0400103 globals.rafScheduler.scheduleRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400104 };
105 vnodeDom.dom.parentElement!.addEventListener(
106 'scroll', this.parentOnScroll, {passive: true});
107 }
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400108 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400109
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400110 onremove({attrs, dom}: m.CVnodeDOM<Attrs>) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400111 window.removeEventListener('resize', this.onResize);
112 globals.rafScheduler.removeRedrawCallback(this.canvasRedrawer);
113 if (attrs.doesScroll) {
114 dom.parentElement!.removeEventListener('scroll', this.parentOnScroll);
115 }
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400116 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400117
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400118 view({attrs}: m.CVnode<Attrs>) {
119 // We receive a new vnode object with new attrs on every mithril redraw. We
120 // store the latest attrs so redrawCanvas can use it.
121 this.attrs = attrs;
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400122 return m(
123 '.scroll-limiter',
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400124 m('canvas.main-canvas'),
125 attrs.panels.map(panel => m('.panel', panel)));
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400126 }
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400127
128 onupdate(vnodeDom: m.CVnodeDOM<Attrs>) {
129 this.repositionCanvas(vnodeDom);
130
131 if (this.updatePanelHeightsFromDom(vnodeDom)) {
132 (vnodeDom.dom as HTMLElement).style.height = `${this.totalPanelHeight}px`;
133 }
134
135 // In non-scrolling case, canvas height can change if panel heights changed.
136 const canvasHeight = this.getCanvasHeight(vnodeDom.attrs.doesScroll);
137 if (this.canvasHeight !== canvasHeight) {
138 this.canvasHeight = canvasHeight;
139 this.updateCanvasDimensions(vnodeDom);
140 }
141 }
142
143 private updateCanvasDimensions(vnodeDom: m.CVnodeDOM<Attrs>) {
144 const canvas =
145 assertExists(vnodeDom.dom.querySelector('canvas.main-canvas')) as
146 HTMLCanvasElement;
147 const ctx = assertExists(this.ctx);
148 canvas.style.height = `${this.canvasHeight}px`;
149 const dpr = window.devicePixelRatio;
150 ctx.canvas.width = this.parentWidth * dpr;
151 ctx.canvas.height = this.canvasHeight * dpr;
152 ctx.scale(dpr, dpr);
153 }
154
155 private updatePanelHeightsFromDom(vnodeDom: m.CVnodeDOM<Attrs>): boolean {
156 const prevHeight = this.totalPanelHeight;
157 this.panelHeights = [];
158 this.totalPanelHeight = 0;
159
160 const panels = vnodeDom.dom.querySelectorAll('.panel');
161 assertTrue(panels.length === vnodeDom.attrs.panels.length);
162 for (let i = 0; i < panels.length; i++) {
163 const height = panels[i].getBoundingClientRect().height;
164 this.panelHeights[i] = height;
165 this.totalPanelHeight += height;
166 }
167
168 return this.totalPanelHeight !== prevHeight;
169 }
170
171 private getCanvasHeight(doesScroll: boolean) {
172 return doesScroll ? this.parentHeight * this.canvasOverdrawFactor :
173 this.totalPanelHeight;
174 }
175
176 private repositionCanvas(vnodeDom: m.CVnodeDOM<Attrs>) {
177 const canvas =
178 assertExists(vnodeDom.dom.querySelector('canvas.main-canvas')) as
179 HTMLCanvasElement;
180 const canvasYStart = this.scrollTop - this.getCanvasOverdrawHeightPerSide();
181 canvas.style.transform = `translateY(${canvasYStart}px)`;
182 }
183
184 private overlapsCanvas(yStart: number, yEnd: number) {
185 return yEnd > 0 && yStart < this.canvasHeight;
186 }
187
188 private redrawCanvas() {
189 if (!this.ctx) return;
190 this.ctx.clearRect(0, 0, this.parentWidth, this.canvasHeight);
191 const canvasYStart = this.scrollTop - this.getCanvasOverdrawHeightPerSide();
192
193 let panelYStart = 0;
194 const panels = assertExists(this.attrs).panels;
195 assertTrue(panels.length === this.panelHeights.length);
196 for (let i = 0; i < panels.length; i++) {
197 const panel = panels[i];
198 const panelHeight = this.panelHeights[i];
199 const yStartOnCanvas = panelYStart - canvasYStart;
200
201 if (!this.overlapsCanvas(yStartOnCanvas, yStartOnCanvas + panelHeight)) {
202 panelYStart += panelHeight;
203 continue;
204 }
205
206 if (!isPanelVNode(panel)) {
207 throw Error('Vnode passed to panel container is not a panel');
208 }
209
210 this.ctx.save();
211 this.ctx.translate(0, yStartOnCanvas);
212 const clipRect = new Path2D();
213 const size = {width: this.parentWidth, height: panelHeight};
214 clipRect.rect(0, 0, size.width, size.height);
215 this.ctx.clip(clipRect);
216 panel.state.renderCanvas(this.ctx, size, panel);
217 this.ctx.restore();
218 panelYStart += panelHeight;
219 }
220 }
221
222 private getCanvasOverdrawHeightPerSide() {
223 const overdrawHeight = (this.canvasOverdrawFactor - 1) * this.parentHeight;
224 return overdrawHeight / 2;
225 }
226}