mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
9cdf8a99ed
* Facebook -> Meta in copyright rg --files | xargs sed -i 's#Copyright (c) Facebook, Inc. and its affiliates.#Copyright (c) Meta Platforms, Inc. and affiliates.#g' * Manual tweaks
246 lines
6.6 KiB
JavaScript
246 lines
6.6 KiB
JavaScript
/**
|
|
* 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 {Size, IntrinsicSize, Rect} from './geometry';
|
|
import type {
|
|
Interaction,
|
|
MouseDownInteraction,
|
|
MouseMoveInteraction,
|
|
MouseUpInteraction,
|
|
WheelPlainInteraction,
|
|
WheelWithShiftInteraction,
|
|
} from './useCanvasInteraction';
|
|
import type {ScrollState} from './utils/scrollState';
|
|
import type {ViewRefs} from './Surface';
|
|
import type {ViewState} from '../types';
|
|
|
|
import {Surface} from './Surface';
|
|
import {View} from './View';
|
|
import {rectContainsPoint} from './geometry';
|
|
import {
|
|
clampState,
|
|
moveStateToRange,
|
|
areScrollStatesEqual,
|
|
translateState,
|
|
zoomState,
|
|
} from './utils/scrollState';
|
|
import {
|
|
MAX_ZOOM_LEVEL,
|
|
MIN_ZOOM_LEVEL,
|
|
MOVE_WHEEL_DELTA_THRESHOLD,
|
|
} from './constants';
|
|
|
|
export class HorizontalPanAndZoomView extends View {
|
|
_contentView: View;
|
|
_intrinsicContentWidth: number;
|
|
_isPanning: boolean = false;
|
|
_viewState: ViewState;
|
|
|
|
constructor(
|
|
surface: Surface,
|
|
frame: Rect,
|
|
contentView: View,
|
|
intrinsicContentWidth: number,
|
|
viewState: ViewState,
|
|
) {
|
|
super(surface, frame);
|
|
|
|
this._contentView = contentView;
|
|
this._intrinsicContentWidth = intrinsicContentWidth;
|
|
this._viewState = viewState;
|
|
|
|
viewState.onHorizontalScrollStateChange(scrollState => {
|
|
this.zoomToRange(scrollState.offset, scrollState.length);
|
|
});
|
|
|
|
this.addSubview(contentView);
|
|
}
|
|
|
|
/**
|
|
* Just sets scroll state.
|
|
* Use `_setStateAndInformCallbacksIfChanged` if this view's callbacks should also be called.
|
|
*
|
|
* @returns Whether state was changed
|
|
* @private
|
|
*/
|
|
setScrollState(proposedState: ScrollState) {
|
|
const clampedState = clampState({
|
|
state: proposedState,
|
|
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
|
|
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
|
|
containerLength: this.frame.size.width,
|
|
});
|
|
if (
|
|
!areScrollStatesEqual(clampedState, this._viewState.horizontalScrollState)
|
|
) {
|
|
this.setNeedsDisplay();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zoom to a specific range of the content specified as a range of the
|
|
* content view's intrinsic content size.
|
|
*
|
|
* Does not inform callbacks of state change since this is a public API.
|
|
*/
|
|
zoomToRange(rangeStart: number, rangeEnd: number) {
|
|
const newState = moveStateToRange({
|
|
state: this._viewState.horizontalScrollState,
|
|
rangeStart,
|
|
rangeEnd,
|
|
contentLength: this._intrinsicContentWidth,
|
|
|
|
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
|
|
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
|
|
containerLength: this.frame.size.width,
|
|
});
|
|
this.setScrollState(newState);
|
|
}
|
|
|
|
desiredSize(): Size | IntrinsicSize {
|
|
return this._contentView.desiredSize();
|
|
}
|
|
|
|
layoutSubviews() {
|
|
const {offset, length} = this._viewState.horizontalScrollState;
|
|
const proposedFrame = {
|
|
origin: {
|
|
x: this.frame.origin.x + offset,
|
|
y: this.frame.origin.y,
|
|
},
|
|
size: {
|
|
width: length,
|
|
height: this.frame.size.height,
|
|
},
|
|
};
|
|
this._contentView.setFrame(proposedFrame);
|
|
super.layoutSubviews();
|
|
}
|
|
|
|
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
|
switch (interaction.type) {
|
|
case 'mousedown':
|
|
this._handleMouseDown(interaction, viewRefs);
|
|
break;
|
|
case 'mousemove':
|
|
this._handleMouseMove(interaction, viewRefs);
|
|
break;
|
|
case 'mouseup':
|
|
this._handleMouseUp(interaction, viewRefs);
|
|
break;
|
|
case 'wheel-plain':
|
|
case 'wheel-shift':
|
|
this._handleWheel(interaction);
|
|
break;
|
|
}
|
|
}
|
|
|
|
_handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
|
|
if (rectContainsPoint(interaction.payload.location, this.frame)) {
|
|
this._isPanning = true;
|
|
|
|
viewRefs.activeView = this;
|
|
|
|
this.currentCursor = 'grabbing';
|
|
}
|
|
}
|
|
|
|
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
|
|
const isHovered = rectContainsPoint(
|
|
interaction.payload.location,
|
|
this.frame,
|
|
);
|
|
if (isHovered && viewRefs.hoveredView === null) {
|
|
viewRefs.hoveredView = this;
|
|
}
|
|
|
|
if (viewRefs.activeView === this) {
|
|
this.currentCursor = 'grabbing';
|
|
} else if (isHovered) {
|
|
this.currentCursor = 'grab';
|
|
}
|
|
|
|
if (!this._isPanning) {
|
|
return;
|
|
}
|
|
|
|
// Don't prevent mouse-move events from bubbling if they are vertical drags.
|
|
const {movementX, movementY} = interaction.payload.event;
|
|
if (Math.abs(movementX) < Math.abs(movementY)) {
|
|
return;
|
|
}
|
|
|
|
const newState = translateState({
|
|
state: this._viewState.horizontalScrollState,
|
|
delta: movementX,
|
|
containerLength: this.frame.size.width,
|
|
});
|
|
this._viewState.updateHorizontalScrollState(newState);
|
|
}
|
|
|
|
_handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
|
|
if (this._isPanning) {
|
|
this._isPanning = false;
|
|
}
|
|
|
|
if (viewRefs.activeView === this) {
|
|
viewRefs.activeView = null;
|
|
}
|
|
}
|
|
|
|
_handleWheel(interaction: WheelPlainInteraction | WheelWithShiftInteraction) {
|
|
const {
|
|
location,
|
|
delta: {deltaX, deltaY},
|
|
} = interaction.payload;
|
|
|
|
if (!rectContainsPoint(location, this.frame)) {
|
|
return; // Not scrolling on view
|
|
}
|
|
|
|
const absDeltaX = Math.abs(deltaX);
|
|
const absDeltaY = Math.abs(deltaY);
|
|
|
|
// Vertical scrolling zooms in and out (unless the SHIFT modifier is used).
|
|
// Horizontal scrolling pans.
|
|
if (absDeltaY > absDeltaX) {
|
|
if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) {
|
|
return;
|
|
}
|
|
|
|
if (interaction.type === 'wheel-shift') {
|
|
// Shift modifier is for scrolling, not zooming.
|
|
return;
|
|
}
|
|
|
|
const newState = zoomState({
|
|
state: this._viewState.horizontalScrollState,
|
|
multiplier: 1 + 0.005 * -deltaY,
|
|
fixedPoint: location.x - this._viewState.horizontalScrollState.offset,
|
|
|
|
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
|
|
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
|
|
containerLength: this.frame.size.width,
|
|
});
|
|
this._viewState.updateHorizontalScrollState(newState);
|
|
} else {
|
|
if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) {
|
|
return;
|
|
}
|
|
|
|
const newState = translateState({
|
|
state: this._viewState.horizontalScrollState,
|
|
delta: -deltaX,
|
|
containerLength: this.frame.size.width,
|
|
});
|
|
this._viewState.updateHorizontalScrollState(newState);
|
|
}
|
|
}
|
|
}
|