blob: 04db42946073c7c9804dd5fcbd18ac8bb90dc18e [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
17import {assertExists} from '../base/logging';
18
19import {globals} from './globals';
20import {Panel} from './panel';
21
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
28function getCanvasOverdrawHeightPerSide(vnode: PanelContainerVnode) {
29 const overdrawHeight =
30 (vnode.state.canvasOverdrawFactor - 1) * vnode.state.parentHeight;
31 return overdrawHeight / 2;
32}
33
34function updateDimensionsFromDom(vnodeDom: PanelContainerVnodeDom) {
35 // Get height fron the parent element.
36 const rect = vnodeDom.dom.parentElement!.getBoundingClientRect();
37 vnodeDom.state.parentWidth = rect.width;
38 vnodeDom.state.parentHeight = rect.height;
39 const dpr = window.devicePixelRatio;
40 const ctx = assertExists(vnodeDom.state.ctx);
41 ctx.canvas.width = vnodeDom.state.parentWidth * dpr;
42 ctx.canvas.height =
43 vnodeDom.state.parentHeight * vnodeDom.state.canvasOverdrawFactor * dpr;
44 ctx.scale(dpr, dpr);
45}
46
47function panelIsOnCanvas(
48 panelYBoundsOnCanvas: {start: number, end: number}, canvasHeight: number) {
49 return panelYBoundsOnCanvas.end > 0 &&
50 panelYBoundsOnCanvas.start < canvasHeight;
51}
52
53
54function renderPanelCanvas(
55 ctx: CanvasRenderingContext2D,
56 width: number,
57 yStartOnCanvas: number,
58 panel: Panel) {
59 ctx.save();
60 ctx.translate(0, yStartOnCanvas);
61 const clipRect = new Path2D();
62 clipRect.rect(0, 0, width, panel.getHeight());
63 ctx.clip(clipRect);
64
65 panel.renderCanvas(ctx);
66
67 ctx.restore();
68}
69
70function redrawAllPanelCavases(vnode: PanelContainerVnode) {
71 const state = vnode.state;
72 if (!state.ctx) return;
73 const canvasHeight = state.parentHeight * state.canvasOverdrawFactor;
74 state.ctx.clearRect(0, 0, state.parentWidth, canvasHeight);
75 const canvasYStart = state.scrollTop - getCanvasOverdrawHeightPerSide(vnode);
76
77 let panelYStart = 0;
78 for (const panel of vnode.attrs.panels) {
79 const yStartOnCanvas = panelYStart - canvasYStart;
80 const panelHeight = panel.getHeight();
81 const panelYBoundsOnCanvas = {
82 start: yStartOnCanvas,
83 end: yStartOnCanvas + panelHeight,
84 };
85 if (!panelIsOnCanvas(panelYBoundsOnCanvas, canvasHeight)) {
86 panelYStart += panelHeight;
87 continue;
88 }
89
90 renderPanelCanvas(state.ctx, state.parentWidth, yStartOnCanvas, panel);
91 panelYStart += panelHeight;
92 }
93}
94
95function repositionCanvas(vnodeDom: PanelContainerVnodeDom) {
96 const canvas =
97 assertExists(vnodeDom.dom.querySelector('canvas.main-canvas')) as
98 HTMLElement;
99 const canvasYStart =
100 vnodeDom.state.scrollTop - getCanvasOverdrawHeightPerSide(vnodeDom);
101 canvas.style.transform = `translateY(${canvasYStart}px)`;
102}
103
104const PanelComponent = {
105 view({attrs}) {
106 return m('.panel', {
107 style: {height: `${attrs.panel.getHeight()}px`},
108 });
109 },
110
111 oncreate({dom, attrs}) {
112 attrs.panel.updateDom(dom as HTMLElement);
113 },
114
115 onupdate({dom, attrs}) {
116 attrs.panel.updateDom(dom as HTMLElement);
117 }
118
119} as m.Component<{panel: Panel}>;
120
121interface PanelContainerState {
122 parentWidth: number;
123 parentHeight: number;
124 scrollTop: number;
125 canvasOverdrawFactor: number;
126 ctx: CanvasRenderingContext2D|null;
127 panels: Panel[];
128
129 // We store these functions so we can remove them.
130 onResize: () => void;
131 canvasRedrawer: () => void;
132 parentOnScroll: () => void;
133}
134
135interface PanelContainerAttrs {
136 panels: Panel[];
137 doesScroll: boolean;
138}
139
140// Vnode contains state + attrs. VnodeDom contains state + attrs + dom.
141type PanelContainerVnode = m.Vnode<PanelContainerAttrs, PanelContainerState>;
142type PanelContainerVnodeDom =
143 m.VnodeDOM<PanelContainerAttrs, PanelContainerState>;
144
145export const PanelContainer = {
146 oninit(vnode: PanelContainerVnode) {
147 // These values are updated with proper values in oncreate.
148 this.parentWidth = 0;
149 this.parentHeight = 0;
150 this.scrollTop = 0;
151 this.canvasOverdrawFactor =
152 vnode.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
153 this.ctx = null;
154 this.canvasRedrawer = () => redrawAllPanelCavases(vnode);
155 this.panels = [];
156 globals.rafScheduler.addRedrawCallback(this.canvasRedrawer);
157 },
158
159 oncreate(vnodeDom: PanelContainerVnodeDom) {
160 // Save the canvas context in the state.
161 const canvas =
162 vnodeDom.dom.querySelector('.main-canvas') as HTMLCanvasElement;
163 const ctx = canvas.getContext('2d');
164 if (!ctx) {
165 throw Error('Cannot create canvas context');
166 }
167 this.ctx = ctx;
168
169 // Calling m.redraw during a lifecycle method results in undefined behavior.
170 // Use setTimeout to do it asyncronously at the end of the current redraw.
171 setTimeout(() => {
172 updateDimensionsFromDom(vnodeDom);
Deepanjan Roy112ff6a2018-09-10 08:31:43 -0400173 globals.rafScheduler.scheduleFullRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400174 });
175
176 // Save the resize handler in the state so we can remove it later.
177 // TODO: Encapsulate resize handling better.
178 this.onResize = () => {
179 updateDimensionsFromDom(vnodeDom);
Deepanjan Roy112ff6a2018-09-10 08:31:43 -0400180 globals.rafScheduler.scheduleFullRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400181 };
182
183 // Once ResizeObservers are out, we can stop accessing the window here.
184 window.addEventListener('resize', this.onResize);
185
186 if (vnodeDom.attrs.doesScroll) {
187 this.parentOnScroll = () => {
188 vnodeDom.state.scrollTop = vnodeDom.dom.parentElement!.scrollTop;
189 repositionCanvas(vnodeDom);
Deepanjan Royf190cb22018-08-28 10:43:07 -0400190 globals.rafScheduler.scheduleRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400191 };
192 vnodeDom.dom.parentElement!.addEventListener(
193 'scroll', this.parentOnScroll, {passive: true});
194 }
195 },
196
197 onremove({attrs, dom}) {
198 window.removeEventListener('resize', this.onResize);
199 globals.rafScheduler.removeRedrawCallback(this.canvasRedrawer);
200 if (attrs.doesScroll) {
201 dom.parentElement!.removeEventListener('scroll', this.parentOnScroll);
202 }
203 },
204
205 view({attrs}) {
206 const totalHeight =
207 attrs.panels.reduce((sum, panel) => sum + panel.getHeight(), 0);
208 const canvasHeight = this.parentHeight * this.canvasOverdrawFactor;
209
210 // In the scrolling case, since the canvas is overdrawn and continuously
211 // repositioned, we need the canvas to be in a div with overflow hidden and
212 // height equaling the total height of the content to prevent scrolling
213 // height from growing.
214 return m(
215 '.scroll-limiter',
216 {
217 style: {
218 height: `${totalHeight}px`,
219 }
220 },
221 m('canvas.main-canvas', {
222 style: {
223 height: `${canvasHeight}px`,
224 }
225 }),
226 attrs.panels.map(panel => m(PanelComponent, {panel, key: panel.id})));
227 },
228
229 onupdate(vnodeDom: PanelContainerVnodeDom) {
230 repositionCanvas(vnodeDom);
231 }
232} as m.Component<PanelContainerAttrs, PanelContainerState>;