mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
344 lines
8.8 KiB
JavaScript
344 lines
8.8 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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 {IntrinsicSize, Rect, Size} from './geometry';
|
|
import type {Layouter} from './layouter';
|
|
import type {ViewRefs} from './Surface';
|
|
|
|
import {Surface} from './Surface';
|
|
import {
|
|
rectEqualToRect,
|
|
intersectionOfRects,
|
|
rectIntersectsRect,
|
|
sizeIsEmpty,
|
|
sizeIsValid,
|
|
unionOfRects,
|
|
zeroRect,
|
|
} from './geometry';
|
|
import {noopLayout, viewsToLayout, collapseLayoutIntoViews} from './layouter';
|
|
|
|
/**
|
|
* Base view class that can be subclassed to draw custom content or manage
|
|
* subclasses.
|
|
*/
|
|
export class View {
|
|
_backgroundColor: string | null;
|
|
|
|
currentCursor: string | null = null;
|
|
|
|
surface: Surface;
|
|
|
|
frame: Rect;
|
|
visibleArea: Rect;
|
|
|
|
superview: ?View;
|
|
subviews: View[] = [];
|
|
|
|
/**
|
|
* An injected function that lays out our subviews.
|
|
* @private
|
|
*/
|
|
_layouter: Layouter;
|
|
|
|
/**
|
|
* Whether this view needs to be drawn.
|
|
*
|
|
* NOTE: Do not set directly! Use `setNeedsDisplay`.
|
|
*
|
|
* @see setNeedsDisplay
|
|
* @private
|
|
*/
|
|
_needsDisplay = true;
|
|
|
|
/**
|
|
* Whether the hierarchy below this view has subviews that need display.
|
|
*
|
|
* NOTE: Do not set directly! Use `setSubviewsNeedDisplay`.
|
|
*
|
|
* @see setSubviewsNeedDisplay
|
|
* @private
|
|
*/
|
|
_subviewsNeedDisplay = false;
|
|
|
|
constructor(
|
|
surface: Surface,
|
|
frame: Rect,
|
|
layouter: Layouter = noopLayout,
|
|
visibleArea: Rect = frame,
|
|
backgroundColor?: string | null = null,
|
|
) {
|
|
this._backgroundColor = backgroundColor || null;
|
|
this.surface = surface;
|
|
this.frame = frame;
|
|
this._layouter = layouter;
|
|
this.visibleArea = visibleArea;
|
|
}
|
|
|
|
/**
|
|
* Invalidates view's contents.
|
|
*
|
|
* Downward propagating; once called, all subviews of this view should also
|
|
* be invalidated.
|
|
*/
|
|
setNeedsDisplay() {
|
|
this._needsDisplay = true;
|
|
if (this.superview) {
|
|
this.superview._setSubviewsNeedDisplay();
|
|
}
|
|
this.subviews.forEach(subview => subview.setNeedsDisplay());
|
|
}
|
|
|
|
/**
|
|
* Informs superview that it has subviews that need to be drawn.
|
|
*
|
|
* Upward propagating; once called, all superviews of this view should also
|
|
* have `subviewsNeedDisplay` = true.
|
|
*
|
|
* @private
|
|
*/
|
|
_setSubviewsNeedDisplay() {
|
|
this._subviewsNeedDisplay = true;
|
|
if (this.superview) {
|
|
this.superview._setSubviewsNeedDisplay();
|
|
}
|
|
}
|
|
|
|
setFrame(newFrame: Rect) {
|
|
if (!rectEqualToRect(this.frame, newFrame)) {
|
|
this.frame = newFrame;
|
|
if (sizeIsValid(newFrame.size)) {
|
|
this.frame = newFrame;
|
|
} else {
|
|
this.frame = zeroRect;
|
|
}
|
|
this.setNeedsDisplay();
|
|
}
|
|
}
|
|
|
|
setVisibleArea(newVisibleArea: Rect) {
|
|
if (!rectEqualToRect(this.visibleArea, newVisibleArea)) {
|
|
if (sizeIsValid(newVisibleArea.size)) {
|
|
this.visibleArea = newVisibleArea;
|
|
} else {
|
|
this.visibleArea = zeroRect;
|
|
}
|
|
this.setNeedsDisplay();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A size that can be used as a hint by layout functions.
|
|
*
|
|
* Implementations should typically return the intrinsic content size or a
|
|
* size that fits all the view's content.
|
|
*
|
|
* The default implementation returns a size that fits all the view's
|
|
* subviews.
|
|
*
|
|
* Can be overridden by subclasses.
|
|
*/
|
|
desiredSize(): Size | IntrinsicSize {
|
|
if (this._needsDisplay) {
|
|
this.layoutSubviews();
|
|
}
|
|
const frames = this.subviews.map(subview => subview.frame);
|
|
return unionOfRects(...frames).size;
|
|
}
|
|
|
|
/**
|
|
* Appends `view` to the list of this view's `subviews`.
|
|
*/
|
|
addSubview(view: View) {
|
|
if (this.subviews.includes(view)) {
|
|
return;
|
|
}
|
|
this.subviews.push(view);
|
|
view.superview = this;
|
|
}
|
|
|
|
/**
|
|
* Breaks the subview-superview relationship between `view` and this view, if
|
|
* `view` is a subview of this view.
|
|
*/
|
|
removeSubview(view: View) {
|
|
const subviewIndex = this.subviews.indexOf(view);
|
|
if (subviewIndex === -1) {
|
|
return;
|
|
}
|
|
view.superview = undefined;
|
|
this.subviews.splice(subviewIndex, 1);
|
|
}
|
|
|
|
/**
|
|
* Removes all subviews from this view.
|
|
*/
|
|
removeAllSubviews() {
|
|
this.subviews.forEach(subview => (subview.superview = undefined));
|
|
this.subviews = [];
|
|
}
|
|
|
|
/**
|
|
* Executes the display flow if this view needs to be drawn.
|
|
*
|
|
* 1. Lays out subviews with `layoutSubviews`.
|
|
* 2. Draws content with `draw`.
|
|
*/
|
|
displayIfNeeded(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
|
|
if (
|
|
(this._needsDisplay || this._subviewsNeedDisplay) &&
|
|
rectIntersectsRect(this.frame, this.visibleArea) &&
|
|
!sizeIsEmpty(this.visibleArea.size)
|
|
) {
|
|
this.layoutSubviews();
|
|
if (this._needsDisplay) this._needsDisplay = false;
|
|
if (this._subviewsNeedDisplay) this._subviewsNeedDisplay = false;
|
|
|
|
// Clip anything drawn by the view to prevent it from overflowing its visible area.
|
|
const visibleArea = this.visibleArea;
|
|
const region = new Path2D();
|
|
region.rect(
|
|
visibleArea.origin.x,
|
|
visibleArea.origin.y,
|
|
visibleArea.size.width,
|
|
visibleArea.size.height,
|
|
);
|
|
context.save();
|
|
context.clip(region);
|
|
context.beginPath();
|
|
|
|
this.draw(context, viewRefs);
|
|
|
|
// Stop clipping
|
|
context.restore();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Layout self and subviews.
|
|
*
|
|
* Implementations should call `setNeedsDisplay` if a draw is required.
|
|
*
|
|
* The default implementation uses `this.layouter` to lay out subviews.
|
|
*
|
|
* Can be overwritten by subclasses that wish to manually manage their
|
|
* subviews' layout.
|
|
*
|
|
* NOTE: Do not call directly! Use `displayIfNeeded`.
|
|
*
|
|
* @see displayIfNeeded
|
|
*/
|
|
layoutSubviews() {
|
|
const {frame, _layouter, subviews, visibleArea} = this;
|
|
const existingLayout = viewsToLayout(subviews);
|
|
const newLayout = _layouter(existingLayout, frame);
|
|
collapseLayoutIntoViews(newLayout);
|
|
|
|
subviews.forEach((subview, subviewIndex) => {
|
|
if (rectIntersectsRect(visibleArea, subview.frame)) {
|
|
subview.setVisibleArea(intersectionOfRects(visibleArea, subview.frame));
|
|
} else {
|
|
subview.setVisibleArea(zeroRect);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Draw the contents of this view in the given canvas `context`.
|
|
*
|
|
* Defaults to drawing this view's `subviews`.
|
|
*
|
|
* To be overwritten by subclasses that wish to draw custom content.
|
|
*
|
|
* NOTE: Do not call directly! Use `displayIfNeeded`.
|
|
*
|
|
* @see displayIfNeeded
|
|
*/
|
|
draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
|
|
const {subviews, visibleArea} = this;
|
|
subviews.forEach(subview => {
|
|
if (rectIntersectsRect(visibleArea, subview.visibleArea)) {
|
|
subview.displayIfNeeded(context, viewRefs);
|
|
}
|
|
});
|
|
|
|
const backgroundColor = this._backgroundColor;
|
|
if (backgroundColor !== null) {
|
|
const desiredSize = this.desiredSize();
|
|
if (visibleArea.size.height > desiredSize.height) {
|
|
context.fillStyle = backgroundColor;
|
|
context.fillRect(
|
|
visibleArea.origin.x,
|
|
visibleArea.origin.y + desiredSize.height,
|
|
visibleArea.size.width,
|
|
visibleArea.size.height - desiredSize.height,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle an `interaction`.
|
|
*
|
|
* To be overwritten by subclasses that wish to handle interactions.
|
|
*
|
|
* NOTE: Do not call directly! Use `handleInteractionAndPropagateToSubviews`
|
|
*/
|
|
handleInteraction(interaction: Interaction, viewRefs: ViewRefs): ?boolean {}
|
|
|
|
/**
|
|
* Handle an `interaction` and propagates it to all of this view's
|
|
* `subviews`.
|
|
*
|
|
* NOTE: Should not be overridden! Subclasses should override
|
|
* `handleInteraction` instead.
|
|
*
|
|
* @see handleInteraction
|
|
* @protected
|
|
*/
|
|
handleInteractionAndPropagateToSubviews(
|
|
interaction: Interaction,
|
|
viewRefs: ViewRefs,
|
|
): boolean {
|
|
const {subviews, visibleArea} = this;
|
|
|
|
if (visibleArea.size.height === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Pass the interaction to subviews first,
|
|
// so they have the opportunity to claim it before it bubbles.
|
|
//
|
|
// Views are painted first to last,
|
|
// so they should process interactions last to first,
|
|
// so views in front (on top) can claim the interaction first.
|
|
for (let i = subviews.length - 1; i >= 0; i--) {
|
|
const subview = subviews[i];
|
|
if (rectIntersectsRect(visibleArea, subview.visibleArea)) {
|
|
const didSubviewHandle =
|
|
subview.handleInteractionAndPropagateToSubviews(
|
|
interaction,
|
|
viewRefs,
|
|
) === true;
|
|
if (didSubviewHandle) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const didSelfHandle =
|
|
this.handleInteraction(interaction, viewRefs) === true;
|
|
if (didSelfHandle) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|