/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Interaction} from './useCanvasInteraction';
import type {Size} from './geometry';
import memoize from 'memoize-one';
import {View} from './View';
import {zeroPoint} from './geometry';
import {DPR} from '../content-views/constants';
export type ViewRefs = {
activeView: View | null,
hoveredView: View | null,
};
// hidpi canvas: https://www.html5rocks.com/en/tutorials/canvas/hidpi/
function configureRetinaCanvas(
canvas: HTMLCanvasElement,
height: number,
width: number,
) {
canvas.width = width * DPR;
canvas.height = height * DPR;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
}
const getCanvasContext = memoize(
(
canvas: HTMLCanvasElement,
height: number,
width: number,
scaleCanvas: boolean = true,
): CanvasRenderingContext2D => {
const context = canvas.getContext('2d', {alpha: false});
if (scaleCanvas) {
configureRetinaCanvas(canvas, height, width);
// Scale all drawing operations by the dpr, so you don't have to worry about the difference.
context.scale(DPR, DPR);
}
return context;
},
);
type ResetHoveredEventFn = () => void;
/**
* Represents the canvas surface and a view heirarchy. A surface is also the
* place where all interactions enter the view heirarchy.
*/
export class Surface {
rootView: ?View;
_context: ?CanvasRenderingContext2D;
_canvasSize: ?Size;
_resetHoveredEvent: ResetHoveredEventFn;
_viewRefs: ViewRefs = {
activeView: null,
hoveredView: null,
};
constructor(resetHoveredEvent: ResetHoveredEventFn) {
this._resetHoveredEvent = resetHoveredEvent;
}
hasActiveView(): boolean {
return this._viewRefs.activeView !== null;
}
setCanvas(canvas: HTMLCanvasElement, canvasSize: Size) {
this._context = getCanvasContext(
canvas,
canvasSize.height,
canvasSize.width,
);
this._canvasSize = canvasSize;
if (this.rootView) {
this.rootView.setNeedsDisplay();
}
}
displayIfNeeded() {
const {rootView, _canvasSize, _context} = this;
if (!rootView || !_context || !_canvasSize) {
return;
}
rootView.setFrame({
origin: zeroPoint,
size: _canvasSize,
});
rootView.setVisibleArea({
origin: zeroPoint,
size: _canvasSize,
});
rootView.displayIfNeeded(_context, this._viewRefs);
}
getCurrentCursor(): string | null {
const {activeView, hoveredView} = this._viewRefs;
if (activeView !== null) {
return activeView.currentCursor;
} else if (hoveredView !== null) {
return hoveredView.currentCursor;
} else {
return null;
}
}
handleInteraction(interaction: Interaction) {
const rootView = this.rootView;
if (rootView != null) {
const viewRefs = this._viewRefs;
switch (interaction.type) {
case 'mousemove':
case 'wheel-control':
case 'wheel-meta':
case 'wheel-plain':
case 'wheel-shift':
// Clean out the hovered view before processing this type of interaction.
const hoveredView = viewRefs.hoveredView;
viewRefs.hoveredView = null;
rootView.handleInteractionAndPropagateToSubviews(
interaction,
viewRefs,
);
// If a previously hovered view is no longer hovered, update the outer state.
if (hoveredView !== null && viewRefs.hoveredView === null) {
this._resetHoveredEvent();
}
break;
default:
rootView.handleInteractionAndPropagateToSubviews(
interaction,
viewRefs,
);
break;
}
}
}
}