• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2024 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
15/**
16 * VirtualCanvas - A Mithril Component for Virtual Canvas Rendering
17 *
18 * This module provides a Mithril component that acts as a scrolling container
19 * for tall and/or wide content. It overlays a floating canvas on top of its
20 * content rendered inside it, which stays in the viewport of scrolling
21 * container element as the user scrolls, allowing for rendering of large-scale
22 * visualizations which would be too large for a normal HTML canvas element.
23 *
24 * Key Features:
25 * - Supports horizontal, vertical, or both axes scrolling, moving the canvas
26 *   while the user scrolls to keep it in the viewport.
27 * - Automatically handles canvas resizing using resize observers, including
28 *   scaling for high DPI displays.
29 * - Calls a callback whenever the canvas needs to be redrawn.
30 */
31
32import m from 'mithril';
33import {DisposableStack} from '../base/disposable_stack';
34import {findRef, toHTMLElement} from '../base/dom_utils';
35import {Rect2D, Size2D} from '../base/geom';
36import {assertExists} from '../base/logging';
37import {VirtualCanvas} from '../base/virtual_canvas';
38
39const CANVAS_CONTAINER_REF = 'canvas-container';
40const CANVAS_OVERDRAW_PX = 300;
41const CANVAS_TOLERANCE_PX = 100;
42
43export interface VirtualOverlayCanvasDrawContext {
44  // Canvas rendering context.
45  readonly ctx: CanvasRenderingContext2D;
46
47  // The size of the virtual canvas element.
48  readonly virtualCanvasSize: Size2D;
49
50  // The rect of the actual canvas W.R.T to the virtual canvas element.
51  readonly canvasRect: Rect2D;
52}
53
54export type Overflow = 'hidden' | 'visible' | 'auto';
55
56export interface VirtualOverlayCanvasAttrs {
57  // Additional class names applied to the root element.
58  readonly className?: string;
59
60  // Overflow rules for the horizontal axis.
61  readonly overflowX?: Overflow;
62
63  // Overflow rules for the vertical axis.
64  readonly overflowY?: Overflow;
65
66  // Called when the canvas needs to be repainted due to a layout shift or
67  // or resize.
68  onCanvasRedraw?(ctx: VirtualOverlayCanvasDrawContext): void;
69
70  // When true the canvas will not be redrawn on mithril update cycles. Disable
71  // this if you want to manage the canvas redraw cycle yourself (i.e. possibly
72  // using raf.addCanvasRedrawCallback()) and want to avoid double redraws.
73  // Default: false.
74  readonly disableCanvasRedrawOnMithrilUpdates?: boolean;
75
76  // Called when the canvas is mounted. The passed redrawCanvas() function can
77  // be called to redraw the canvas synchronously at any time. Any returned
78  // disposable will be disposed of when the component is removed.
79  onMount?(redrawCanvas: () => void): Disposable | void;
80}
81
82function getScrollAxesFromOverflow(x: Overflow, y: Overflow) {
83  if (x === 'auto' && y === 'auto') {
84    return 'both';
85  } else if (x === 'auto') {
86    return 'x';
87  } else if (y === 'auto') {
88    return 'y';
89  } else {
90    //
91    return 'none';
92  }
93}
94
95// This mithril component acts as scrolling container for tall and/or wide
96// content. Adds a virtually scrolling canvas over the top of any child elements
97// rendered inside it.
98export class VirtualOverlayCanvas
99  implements m.ClassComponent<VirtualOverlayCanvasAttrs>
100{
101  readonly trash = new DisposableStack();
102  private ctx?: CanvasRenderingContext2D;
103  private virtualCanvas?: VirtualCanvas;
104  private attrs?: VirtualOverlayCanvasAttrs;
105
106  view({attrs, children}: m.CVnode<VirtualOverlayCanvasAttrs>) {
107    this.attrs = attrs;
108    const {overflowX = 'visible', overflowY = 'visible'} = attrs;
109
110    return m(
111      '.pf-virtual-overlay-canvas', // The scrolling container
112      {
113        className: attrs.className,
114        style: {
115          overflowX,
116          overflowY,
117        },
118      },
119      m(
120        '.pf-virtual-overlay-canvas__content', // Container for scrolling element, used for sizing the canvas
121        children,
122        // Put canvas container after content so it appears on top. An actual
123        // canvas element will be created inside here by the
124        // VirtualCanvasHelper.
125        m('.pf-virtual-overlay-canvas__canvas-container', {
126          ref: CANVAS_CONTAINER_REF,
127        }),
128      ),
129    );
130  }
131
132  oncreate({attrs, dom}: m.CVnodeDOM<VirtualOverlayCanvasAttrs>) {
133    const canvasContainerElement = toHTMLElement(
134      assertExists(findRef(dom, CANVAS_CONTAINER_REF)),
135    );
136    const {overflowX = 'visible', overflowY = 'visible'} = attrs;
137
138    // Create the virtual canvas inside the canvas container element. We assume
139    // the scrolling container is the root level element of this component so we
140    // can just use `dom`.
141    const virtualCanvas = new VirtualCanvas(canvasContainerElement, dom, {
142      overdrawPx: CANVAS_OVERDRAW_PX,
143      tolerancePx: CANVAS_TOLERANCE_PX,
144      overdrawAxes: getScrollAxesFromOverflow(overflowX, overflowY),
145    });
146    this.trash.use(virtualCanvas);
147    this.virtualCanvas = virtualCanvas;
148
149    // Create the canvas rendering context
150    this.ctx = assertExists(virtualCanvas.canvasElement.getContext('2d'));
151
152    // When the container resizes, we might need to resize the canvas. This can
153    // be slow so we don't want to do it every render cycle. VirtualCanvas will
154    // tell us when we need to do this.
155    virtualCanvas.setCanvasResizeListener((canvas, width, height) => {
156      const dpr = window.devicePixelRatio;
157      canvas.width = width * dpr;
158      canvas.height = height * dpr;
159    });
160
161    // Whenever the canvas changes size or moves around (e.g. when scrolling),
162    // we'll need to trigger a re-render to keep canvas content aligned with the
163    // DOM elements underneath.
164    virtualCanvas.setLayoutShiftListener(() => {
165      this.redrawCanvas();
166    });
167
168    const disposable = attrs.onMount?.(this.redrawCanvas.bind(this));
169    disposable && this.trash.use(disposable);
170
171    !attrs.disableCanvasRedrawOnMithrilUpdates && this.redrawCanvas();
172  }
173
174  onupdate({attrs}: m.CVnodeDOM<VirtualOverlayCanvasAttrs>) {
175    !attrs.disableCanvasRedrawOnMithrilUpdates && this.redrawCanvas();
176  }
177
178  onremove() {
179    this.trash.dispose();
180  }
181
182  private redrawCanvas() {
183    const ctx = assertExists(this.ctx);
184    const virtualCanvas = assertExists(this.virtualCanvas);
185
186    // Reset & clear canvas
187    ctx.resetTransform();
188    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
189
190    // Adjust scaling according pixel ratio. This makes sure the canvas remains
191    // sharp on high DPI screens.
192    const dpr = window.devicePixelRatio;
193    ctx.scale(dpr, dpr);
194
195    // Align canvas rendering offset with the canvas container, not the actual
196    // canvas. This means we can ignore the fact that we are using a virtual
197    // canvas and just render assuming (0, 0) is at the top left of the canvas
198    // container.
199    ctx.translate(
200      -virtualCanvas.canvasRect.left,
201      -virtualCanvas.canvasRect.top,
202    );
203
204    assertExists(this.attrs).onCanvasRedraw?.({
205      ctx,
206      virtualCanvasSize: virtualCanvas.size,
207      canvasRect: virtualCanvas.canvasRect,
208    });
209  }
210}
211