mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
DevTools: Scheduling profiler: Add vertical scroll bar (#22005)
Co-authored-by: E-Liang Tan <eliang@eliangtan.com>
This commit is contained in:
co-authored by
E-Liang Tan
parent
88d121899a
commit
280e3f936f
+21
-10
@@ -30,14 +30,12 @@ import {copy} from 'clipboard-js';
|
||||
import prettyMilliseconds from 'pretty-ms';
|
||||
|
||||
import {
|
||||
BackgroundColorView,
|
||||
HorizontalPanAndZoomView,
|
||||
ResizableView,
|
||||
VerticalScrollOverflowView,
|
||||
Surface,
|
||||
VerticalScrollView,
|
||||
View,
|
||||
createComposedLayout,
|
||||
lastViewTakesUpRemainingSpaceLayout,
|
||||
useCanvasInteraction,
|
||||
verticallyStackedLayout,
|
||||
zeroPoint,
|
||||
@@ -325,10 +323,9 @@ function AutoSizedCanvas({
|
||||
const rootView = new View(
|
||||
surface,
|
||||
defaultFrame,
|
||||
createComposedLayout(
|
||||
verticallyStackedLayout,
|
||||
lastViewTakesUpRemainingSpaceLayout,
|
||||
),
|
||||
verticallyStackedLayout,
|
||||
defaultFrame,
|
||||
COLORS.BACKGROUND,
|
||||
);
|
||||
rootView.addSubview(axisMarkersViewWrapper);
|
||||
if (userTimingMarksViewWrapper !== null) {
|
||||
@@ -345,10 +342,14 @@ function AutoSizedCanvas({
|
||||
}
|
||||
rootView.addSubview(flamechartViewWrapper);
|
||||
|
||||
// If subviews are less than the available height, fill remaining height with a solid color.
|
||||
rootView.addSubview(new BackgroundColorView(surface, defaultFrame));
|
||||
const verticalScrollOverflowView = new VerticalScrollOverflowView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
rootView,
|
||||
viewState,
|
||||
);
|
||||
|
||||
surfaceRef.current.rootView = rootView;
|
||||
surfaceRef.current.rootView = verticalScrollOverflowView;
|
||||
}, [data]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -401,6 +402,16 @@ function AutoSizedCanvas({
|
||||
const surface = surfaceRef.current;
|
||||
surface.handleInteraction(interaction);
|
||||
|
||||
// Flush any display work that got queued up as part of the previous interaction.
|
||||
// Typically there should be no work, but certain interactions may need a second pass.
|
||||
// For example, the ResizableView may collapse/expand its contents,
|
||||
// which requires a second layout pass for an ancestor VerticalScrollOverflowView.
|
||||
//
|
||||
// TODO It would be nice to remove this call for performance reasons.
|
||||
// To do that, we'll need to address the UX bug with VerticalScrollOverflowView.
|
||||
// For more info see: https://github.com/facebook/react/pull/22005#issuecomment-896953399
|
||||
surface.displayIfNeeded();
|
||||
|
||||
canvas.style.cursor = surface.getCurrentCursor() || 'default';
|
||||
|
||||
// Defer drawing to canvas until React's commit phase, to avoid drawing
|
||||
|
||||
+4
-2
@@ -241,9 +241,11 @@ function processTimelineEvent(
|
||||
|
||||
// Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant
|
||||
if (
|
||||
type.startsWith('blur') ||
|
||||
type.startsWith('click') ||
|
||||
type === 'blur' ||
|
||||
type === 'click' ||
|
||||
type === 'input' ||
|
||||
type.startsWith('focus') ||
|
||||
type.startsWith('key') ||
|
||||
type.startsWith('mouse') ||
|
||||
type.startsWith('pointer')
|
||||
) {
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
// TODO Vertically stack views (via verticallyStackedLayout).
|
||||
// If stacked views are taller than the available height, a vertical scrollbar will be shown on the side,
|
||||
// and width will be adjusted to subtract the width of the scrollbar.
|
||||
+52
-24
@@ -34,10 +34,16 @@ const CARET_MARGIN = 3;
|
||||
const CARET_WIDTH = 5;
|
||||
const CARET_HEIGHT = 3;
|
||||
|
||||
type OnChangeCallback = (
|
||||
scrollState: ScrollState,
|
||||
containerLength: number,
|
||||
) => void;
|
||||
|
||||
export class VerticalScrollView extends View {
|
||||
_contentView: View;
|
||||
_isPanning: boolean;
|
||||
_mutableViewStateKey: string;
|
||||
_onChangeCallback: OnChangeCallback | null;
|
||||
_scrollState: ScrollState;
|
||||
_viewState: ViewState;
|
||||
|
||||
@@ -53,6 +59,7 @@ export class VerticalScrollView extends View {
|
||||
this._contentView = contentView;
|
||||
this._isPanning = false;
|
||||
this._mutableViewStateKey = label + ':VerticalScrollView';
|
||||
this._onChangeCallback = null;
|
||||
this._scrollState = {
|
||||
offset: 0,
|
||||
length: 0,
|
||||
@@ -146,26 +153,45 @@ export class VerticalScrollView extends View {
|
||||
super.layoutSubviews();
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction) {
|
||||
handleInteraction(interaction: Interaction): ?boolean {
|
||||
switch (interaction.type) {
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction);
|
||||
break;
|
||||
return this._handleMouseDown(interaction);
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction);
|
||||
break;
|
||||
return this._handleMouseMove(interaction);
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction);
|
||||
break;
|
||||
return this._handleMouseUp(interaction);
|
||||
case 'wheel-shift':
|
||||
this._handleWheelShift(interaction);
|
||||
break;
|
||||
return this._handleWheelShift(interaction);
|
||||
}
|
||||
}
|
||||
|
||||
onChange(callback: OnChangeCallback) {
|
||||
this._onChangeCallback = callback;
|
||||
}
|
||||
|
||||
scrollBy(deltaY: number): boolean {
|
||||
const newState = translateState({
|
||||
state: this._scrollState,
|
||||
delta: -deltaY,
|
||||
containerLength: this.frame.size.height,
|
||||
});
|
||||
|
||||
// If the state is updated by this wheel scroll,
|
||||
// return true to prevent the interaction from bubbling.
|
||||
// For instance, this prevents the outermost container from also scrolling.
|
||||
return this._setScrollState(newState);
|
||||
}
|
||||
|
||||
_handleMouseDown(interaction: MouseDownInteraction) {
|
||||
if (rectContainsPoint(interaction.payload.location, this.frame)) {
|
||||
this._isPanning = true;
|
||||
const frameHeight = this.frame.size.height;
|
||||
const contentHeight = this._contentView.desiredSize().height;
|
||||
// Don't claim drag operations if the content is not tall enough to be scrollable.
|
||||
// This would block any outer scroll views from working.
|
||||
if (frameHeight < contentHeight) {
|
||||
this._isPanning = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +205,7 @@ export class VerticalScrollView extends View {
|
||||
containerLength: this.frame.size.height,
|
||||
});
|
||||
this._setScrollState(newState);
|
||||
return true;
|
||||
}
|
||||
|
||||
_handleMouseUp(interaction: MouseUpInteraction) {
|
||||
@@ -187,31 +214,27 @@ export class VerticalScrollView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
_handleWheelShift(interaction: WheelWithShiftInteraction) {
|
||||
_handleWheelShift(interaction: WheelWithShiftInteraction): boolean {
|
||||
const {
|
||||
location,
|
||||
delta: {deltaX, deltaY},
|
||||
} = interaction.payload;
|
||||
|
||||
if (!rectContainsPoint(location, this.frame)) {
|
||||
return; // Not scrolling on view
|
||||
return false; // Not scrolling on view
|
||||
}
|
||||
|
||||
const absDeltaX = Math.abs(deltaX);
|
||||
const absDeltaY = Math.abs(deltaY);
|
||||
if (absDeltaX > absDeltaY) {
|
||||
return; // Scrolling horizontally
|
||||
return false; // Scrolling horizontally
|
||||
}
|
||||
|
||||
if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) {
|
||||
return;
|
||||
return false; // Movement was too small and should be ignored.
|
||||
}
|
||||
|
||||
const newState = translateState({
|
||||
state: this._scrollState,
|
||||
delta: -deltaY,
|
||||
containerLength: this.frame.size.height,
|
||||
});
|
||||
this._setScrollState(newState);
|
||||
return this.scrollBy(deltaY);
|
||||
}
|
||||
|
||||
_restoreMutableViewState() {
|
||||
@@ -231,10 +254,7 @@ export class VerticalScrollView extends View {
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_setScrollState(proposedState: ScrollState) {
|
||||
_setScrollState(proposedState: ScrollState): boolean {
|
||||
const height = this._contentView.frame.size.height;
|
||||
const clampedState = clampState({
|
||||
state: proposedState,
|
||||
@@ -247,6 +267,14 @@ export class VerticalScrollView extends View {
|
||||
this._scrollState.length = clampedState.length;
|
||||
|
||||
this.setNeedsDisplay();
|
||||
|
||||
if (this._onChangeCallback !== null) {
|
||||
this._onChangeCallback(clampedState, this.frame.size.height);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ import {noopLayout, viewsToLayout, collapseLayoutIntoViews} from './layouter';
|
||||
* subclasses.
|
||||
*/
|
||||
export class View {
|
||||
_backgroundColor: string | null;
|
||||
|
||||
currentCursor: string | null = null;
|
||||
|
||||
surface: Surface;
|
||||
@@ -70,7 +72,9 @@ export class View {
|
||||
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;
|
||||
@@ -246,6 +250,20 @@ export class View {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,7 +273,7 @@ export class View {
|
||||
*
|
||||
* NOTE: Do not call directly! Use `handleInteractionAndPropagateToSubviews`
|
||||
*/
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {}
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs): ?boolean {}
|
||||
|
||||
/**
|
||||
* Handle an `interaction` and propagates it to all of this view's
|
||||
@@ -270,19 +288,39 @@ export class View {
|
||||
handleInteractionAndPropagateToSubviews(
|
||||
interaction: Interaction,
|
||||
viewRefs: ViewRefs,
|
||||
) {
|
||||
): boolean {
|
||||
const {subviews, visibleArea} = this;
|
||||
|
||||
if (visibleArea.size.height === 0) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.handleInteraction(interaction, viewRefs);
|
||||
|
||||
subviews.forEach(subview => {
|
||||
// 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)) {
|
||||
subview.handleInteractionAndPropagateToSubviews(interaction, viewRefs);
|
||||
const didSubviewHandle =
|
||||
subview.handleInteractionAndPropagateToSubviews(
|
||||
interaction,
|
||||
viewRefs,
|
||||
) === true;
|
||||
if (didSubviewHandle) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const didSelfHandle =
|
||||
this.handleInteraction(interaction, viewRefs) === true;
|
||||
if (didSelfHandle) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
|
||||
export * from './BackgroundColorView';
|
||||
export * from './HorizontalPanAndZoomView';
|
||||
export * from './ResizableView';
|
||||
export * from './Surface';
|
||||
export * from './VerticalScrollView';
|
||||
export * from './View';
|
||||
export * from './geometry';
|
||||
export * from './layouter';
|
||||
export * from './resizable';
|
||||
export * from './useCanvasInteraction';
|
||||
export * from './vertical-scroll-overflow';
|
||||
|
||||
@@ -206,50 +206,6 @@ export const atLeastContainerHeightLayout: Layouter = (
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces last view to take up the space below the second-last view.
|
||||
* Intended to be used with a vertical stack layout.
|
||||
*/
|
||||
export const lastViewTakesUpRemainingSpaceLayout: Layouter = (
|
||||
layout,
|
||||
containerFrame,
|
||||
) => {
|
||||
if (layout.length === 0) {
|
||||
// Nothing to do
|
||||
return layout;
|
||||
}
|
||||
|
||||
if (layout.length === 1) {
|
||||
// No second-last view; the view should just take up the container height
|
||||
return containerHeightLayout(layout, containerFrame);
|
||||
}
|
||||
|
||||
const layoutInfoToPassThrough = layout.slice(0, layout.length - 1);
|
||||
const secondLastLayoutInfo =
|
||||
layoutInfoToPassThrough[layoutInfoToPassThrough.length - 1];
|
||||
|
||||
const remainingHeight =
|
||||
containerFrame.size.height -
|
||||
secondLastLayoutInfo.frame.origin.y -
|
||||
secondLastLayoutInfo.frame.size.height;
|
||||
const height = Math.max(remainingHeight, 0); // Prevent negative heights
|
||||
|
||||
const lastLayoutInfo = layout[layout.length - 1];
|
||||
return [
|
||||
...layoutInfoToPassThrough,
|
||||
{
|
||||
...lastLayoutInfo,
|
||||
frame: {
|
||||
origin: lastLayoutInfo.frame.origin,
|
||||
size: {
|
||||
width: lastLayoutInfo.frame.size.width,
|
||||
height,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a layouter that applies each layouter in `layouters` in sequence.
|
||||
*/
|
||||
|
||||
+25
-191
@@ -14,20 +14,17 @@ import type {
|
||||
MouseDownInteraction,
|
||||
MouseMoveInteraction,
|
||||
MouseUpInteraction,
|
||||
} from './useCanvasInteraction';
|
||||
import type {Rect} from './geometry';
|
||||
import type {ViewRefs} from './Surface';
|
||||
import type {ViewState} from '../types';
|
||||
} from '../useCanvasInteraction';
|
||||
import type {Rect} from '../geometry';
|
||||
import type {ViewRefs} from '../Surface';
|
||||
import type {ViewState} from '../../types';
|
||||
|
||||
import {BORDER_SIZE, COLORS} from '../content-views/constants';
|
||||
import {drawText} from '../content-views/utils/text';
|
||||
import {Surface} from './Surface';
|
||||
import {View} from './View';
|
||||
import {intersectionOfRects, rectContainsPoint} from './geometry';
|
||||
import {noopLayout} from './layouter';
|
||||
import {clamp} from './utils/clamp';
|
||||
|
||||
type ResizeBarState = 'normal' | 'hovered' | 'dragging';
|
||||
import {ResizeBarView} from './ResizeBarView';
|
||||
import {Surface} from '../Surface';
|
||||
import {View} from '../View';
|
||||
import {rectContainsPoint} from '../geometry';
|
||||
import {noopLayout} from '../layouter';
|
||||
import {clamp} from '../utils/clamp';
|
||||
|
||||
type ResizingState = $ReadOnly<{|
|
||||
/** Distance between top of resize bar and mouseY */
|
||||
@@ -41,8 +38,6 @@ type LayoutState = {|
|
||||
barOffsetY: number,
|
||||
|};
|
||||
|
||||
const RESIZE_BAR_DOT_RADIUS = 1;
|
||||
const RESIZE_BAR_DOT_SPACING = 4;
|
||||
const RESIZE_BAR_HEIGHT = 8;
|
||||
const RESIZE_BAR_WITH_LABEL_HEIGHT = 16;
|
||||
|
||||
@@ -51,175 +46,11 @@ const HIDDEN_RECT = {
|
||||
size: {width: 0, height: 0},
|
||||
};
|
||||
|
||||
class ResizeBar extends View {
|
||||
_interactionState: ResizeBarState = 'normal';
|
||||
_label: string;
|
||||
|
||||
showLabel: boolean = false;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, label: string) {
|
||||
super(surface, frame, noopLayout);
|
||||
|
||||
this._label = label;
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this.showLabel
|
||||
? {height: RESIZE_BAR_WITH_LABEL_HEIGHT, width: 0}
|
||||
: {height: RESIZE_BAR_HEIGHT, width: 0};
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
|
||||
const {visibleArea} = this;
|
||||
const {x, y} = visibleArea.origin;
|
||||
const {width, height} = visibleArea.size;
|
||||
|
||||
const isActive =
|
||||
this._interactionState === 'dragging' ||
|
||||
(this._interactionState === 'hovered' && viewRefs.activeView === null);
|
||||
|
||||
context.fillStyle = isActive
|
||||
? COLORS.REACT_RESIZE_BAR_ACTIVE
|
||||
: COLORS.REACT_RESIZE_BAR;
|
||||
context.fillRect(x, y, width, height);
|
||||
|
||||
context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER;
|
||||
context.fillRect(x, y, width, BORDER_SIZE);
|
||||
context.fillRect(x, y + height - BORDER_SIZE, width, BORDER_SIZE);
|
||||
|
||||
const horizontalCenter = x + width / 2;
|
||||
const verticalCenter = y + height / 2;
|
||||
|
||||
if (this.showLabel) {
|
||||
// When the resize view is collapsed entirely,
|
||||
// rather than showing a resize bar– this view displays a label.
|
||||
const labelRect: Rect = {
|
||||
origin: {
|
||||
x: 0,
|
||||
y: y + height - RESIZE_BAR_WITH_LABEL_HEIGHT,
|
||||
},
|
||||
size: {
|
||||
width: visibleArea.size.width,
|
||||
height: visibleArea.size.height,
|
||||
},
|
||||
};
|
||||
|
||||
const drawableRect = intersectionOfRects(labelRect, this.visibleArea);
|
||||
|
||||
drawText(this._label, context, labelRect, drawableRect, {
|
||||
fillStyle: COLORS.REACT_RESIZE_BAR_DOT,
|
||||
textAlign: 'center',
|
||||
});
|
||||
} else {
|
||||
// Otherwise draw horizontally centered resize bar dots
|
||||
context.beginPath();
|
||||
context.fillStyle = COLORS.REACT_RESIZE_BAR_DOT;
|
||||
context.arc(
|
||||
horizontalCenter,
|
||||
verticalCenter,
|
||||
RESIZE_BAR_DOT_RADIUS,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.arc(
|
||||
horizontalCenter + RESIZE_BAR_DOT_SPACING,
|
||||
verticalCenter,
|
||||
RESIZE_BAR_DOT_RADIUS,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.arc(
|
||||
horizontalCenter - RESIZE_BAR_DOT_SPACING,
|
||||
verticalCenter,
|
||||
RESIZE_BAR_DOT_RADIUS,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.fill();
|
||||
}
|
||||
}
|
||||
|
||||
_setInteractionState(state: ResizeBarState) {
|
||||
if (this._interactionState === state) {
|
||||
return;
|
||||
}
|
||||
this._interactionState = state;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
_handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
if (cursorInView) {
|
||||
this._setInteractionState('dragging');
|
||||
viewRefs.activeView = this;
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
|
||||
if (viewRefs.activeView === this) {
|
||||
// If we're actively dragging this resize bar,
|
||||
// show the cursor even if the pointer isn't hovering over this view.
|
||||
this.currentCursor = 'ns-resize';
|
||||
} else if (cursorInView) {
|
||||
if (this.showLabel) {
|
||||
this.currentCursor = 'pointer';
|
||||
} else {
|
||||
this.currentCursor = 'ns-resize';
|
||||
}
|
||||
}
|
||||
|
||||
if (cursorInView) {
|
||||
viewRefs.hoveredView = this;
|
||||
}
|
||||
|
||||
if (this._interactionState === 'dragging') {
|
||||
return;
|
||||
}
|
||||
this._setInteractionState(cursorInView ? 'hovered' : 'normal');
|
||||
}
|
||||
|
||||
_handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
if (this._interactionState === 'dragging') {
|
||||
this._setInteractionState(cursorInView ? 'hovered' : 'normal');
|
||||
}
|
||||
|
||||
if (viewRefs.activeView === this) {
|
||||
viewRefs.activeView = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction, viewRefs);
|
||||
return;
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction, viewRefs);
|
||||
return;
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction, viewRefs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ResizableView extends View {
|
||||
_canvasRef: {current: HTMLCanvasElement | null};
|
||||
_layoutState: LayoutState;
|
||||
_mutableViewStateKey: string;
|
||||
_resizeBar: ResizeBar;
|
||||
_resizeBar: ResizeBarView;
|
||||
_resizingState: ResizingState | null = null;
|
||||
_subview: View;
|
||||
_viewState: ViewState;
|
||||
@@ -238,7 +69,7 @@ export class ResizableView extends View {
|
||||
this._layoutState = {barOffsetY: 0};
|
||||
this._mutableViewStateKey = label + ':ResizableView';
|
||||
this._subview = subview;
|
||||
this._resizeBar = new ResizeBar(surface, frame, label);
|
||||
this._resizeBar = new ResizeBarView(surface, frame, label);
|
||||
this._viewState = viewState;
|
||||
|
||||
this.addSubview(this._subview);
|
||||
@@ -379,6 +210,8 @@ export class ResizableView extends View {
|
||||
const subviewDesiredSize = this._subview.desiredSize();
|
||||
this._updateLayoutStateAndResizeBar(subviewDesiredSize.height);
|
||||
this.setNeedsDisplay();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,6 +230,8 @@ export class ResizableView extends View {
|
||||
// Double clicking on the expanded view should collapse.
|
||||
this._updateLayoutStateAndResizeBar(0);
|
||||
this.setNeedsDisplay();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,6 +245,8 @@ export class ResizableView extends View {
|
||||
cursorOffsetInBarFrame: mouseY - resizeBarFrame.origin.y,
|
||||
mouseY,
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +258,8 @@ export class ResizableView extends View {
|
||||
mouseY: interaction.payload.location.y,
|
||||
};
|
||||
this.setNeedsDisplay();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,20 +282,15 @@ export class ResizableView extends View {
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'click':
|
||||
this._handleClick(interaction);
|
||||
return;
|
||||
return this._handleClick(interaction);
|
||||
case 'double-click':
|
||||
this._handleDoubleClick(interaction);
|
||||
return;
|
||||
return this._handleDoubleClick(interaction);
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction);
|
||||
return;
|
||||
return this._handleMouseDown(interaction);
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction);
|
||||
return;
|
||||
return this._handleMouseMove(interaction);
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction);
|
||||
return;
|
||||
return this._handleMouseUp(interaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 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,
|
||||
MouseDownInteraction,
|
||||
MouseMoveInteraction,
|
||||
MouseUpInteraction,
|
||||
} from '../useCanvasInteraction';
|
||||
import type {Rect} from '../geometry';
|
||||
import type {ViewRefs} from '../Surface';
|
||||
|
||||
import {BORDER_SIZE, COLORS} from '../../content-views/constants';
|
||||
import {drawText} from '../../content-views/utils/text';
|
||||
import {Surface} from '../Surface';
|
||||
import {View} from '../View';
|
||||
import {rectContainsPoint} from '../geometry';
|
||||
import {noopLayout} from '../layouter';
|
||||
|
||||
type ResizeBarState = 'normal' | 'hovered' | 'dragging';
|
||||
|
||||
const RESIZE_BAR_DOT_RADIUS = 1;
|
||||
const RESIZE_BAR_DOT_SPACING = 4;
|
||||
const RESIZE_BAR_HEIGHT = 8;
|
||||
const RESIZE_BAR_WITH_LABEL_HEIGHT = 16;
|
||||
|
||||
export class ResizeBarView extends View {
|
||||
_interactionState: ResizeBarState = 'normal';
|
||||
_label: string;
|
||||
|
||||
showLabel: boolean = false;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, label: string) {
|
||||
super(surface, frame, noopLayout);
|
||||
|
||||
this._label = label;
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this.showLabel
|
||||
? {height: RESIZE_BAR_WITH_LABEL_HEIGHT, width: 0}
|
||||
: {height: RESIZE_BAR_HEIGHT, width: 0};
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
|
||||
const {frame} = this;
|
||||
const {x, y} = frame.origin;
|
||||
const {width, height} = frame.size;
|
||||
|
||||
const isActive =
|
||||
this._interactionState === 'dragging' ||
|
||||
(this._interactionState === 'hovered' && viewRefs.activeView === null);
|
||||
|
||||
context.fillStyle = isActive
|
||||
? COLORS.REACT_RESIZE_BAR_ACTIVE
|
||||
: COLORS.REACT_RESIZE_BAR;
|
||||
context.fillRect(x, y, width, height);
|
||||
|
||||
context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER;
|
||||
context.fillRect(x, y, width, BORDER_SIZE);
|
||||
context.fillRect(x, y + height - BORDER_SIZE, width, BORDER_SIZE);
|
||||
|
||||
const horizontalCenter = x + width / 2;
|
||||
const verticalCenter = y + height / 2;
|
||||
|
||||
if (this.showLabel) {
|
||||
// When the resize view is collapsed entirely,
|
||||
// rather than showing a resize bar– this view displays a label.
|
||||
const labelRect: Rect = {
|
||||
origin: {
|
||||
x: 0,
|
||||
y: y + height - RESIZE_BAR_WITH_LABEL_HEIGHT,
|
||||
},
|
||||
size: {
|
||||
width: frame.size.width,
|
||||
height: RESIZE_BAR_WITH_LABEL_HEIGHT,
|
||||
},
|
||||
};
|
||||
|
||||
drawText(this._label, context, labelRect, frame, {
|
||||
fillStyle: COLORS.REACT_RESIZE_BAR_DOT,
|
||||
textAlign: 'center',
|
||||
});
|
||||
} else {
|
||||
// Otherwise draw horizontally centered resize bar dots
|
||||
context.beginPath();
|
||||
context.fillStyle = COLORS.REACT_RESIZE_BAR_DOT;
|
||||
context.arc(
|
||||
horizontalCenter,
|
||||
verticalCenter,
|
||||
RESIZE_BAR_DOT_RADIUS,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.arc(
|
||||
horizontalCenter + RESIZE_BAR_DOT_SPACING,
|
||||
verticalCenter,
|
||||
RESIZE_BAR_DOT_RADIUS,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.arc(
|
||||
horizontalCenter - RESIZE_BAR_DOT_SPACING,
|
||||
verticalCenter,
|
||||
RESIZE_BAR_DOT_RADIUS,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.fill();
|
||||
}
|
||||
}
|
||||
|
||||
_setInteractionState(state: ResizeBarState) {
|
||||
if (this._interactionState === state) {
|
||||
return;
|
||||
}
|
||||
this._interactionState = state;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
_handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
if (cursorInView) {
|
||||
this._setInteractionState('dragging');
|
||||
viewRefs.activeView = this;
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
|
||||
if (viewRefs.activeView === this) {
|
||||
// If we're actively dragging this resize bar,
|
||||
// show the cursor even if the pointer isn't hovering over this view.
|
||||
this.currentCursor = 'ns-resize';
|
||||
} else if (cursorInView) {
|
||||
if (this.showLabel) {
|
||||
this.currentCursor = 'pointer';
|
||||
} else {
|
||||
this.currentCursor = 'ns-resize';
|
||||
}
|
||||
}
|
||||
|
||||
if (cursorInView) {
|
||||
viewRefs.hoveredView = this;
|
||||
}
|
||||
|
||||
if (this._interactionState === 'dragging') {
|
||||
return;
|
||||
}
|
||||
this._setInteractionState(cursorInView ? 'hovered' : 'normal');
|
||||
}
|
||||
|
||||
_handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
if (this._interactionState === 'dragging') {
|
||||
this._setInteractionState(cursorInView ? 'hovered' : 'normal');
|
||||
}
|
||||
|
||||
if (viewRefs.activeView === this) {
|
||||
viewRefs.activeView = null;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export * from './ResizableView';
|
||||
export * from './ResizeBarView';
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* 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 {Rect} from '../geometry';
|
||||
import type {Surface, ViewRefs} from '../Surface';
|
||||
import type {
|
||||
ClickInteraction,
|
||||
MouseDownInteraction,
|
||||
MouseMoveInteraction,
|
||||
MouseUpInteraction,
|
||||
} from '../useCanvasInteraction';
|
||||
|
||||
import {VerticalScrollOverflowView} from './VerticalScrollOverflowView';
|
||||
import {rectContainsPoint, rectEqualToRect} from '../geometry';
|
||||
import {View} from '../View';
|
||||
import {BORDER_SIZE, COLORS} from '../../content-views/constants';
|
||||
|
||||
const SCROLL_BAR_SIZE = 14;
|
||||
|
||||
const HIDDEN_RECT = {
|
||||
origin: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
size: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export class VerticalScrollBarView extends View {
|
||||
_contentHeight: number = 0;
|
||||
_isScrolling: boolean = false;
|
||||
_scrollBarRect: Rect = HIDDEN_RECT;
|
||||
_scrollThumbRect: Rect = HIDDEN_RECT;
|
||||
_verticalScrollOverflowView: VerticalScrollOverflowView;
|
||||
|
||||
constructor(
|
||||
surface: Surface,
|
||||
frame: Rect,
|
||||
verticalScrollOverflowView: VerticalScrollOverflowView,
|
||||
) {
|
||||
super(surface, frame);
|
||||
|
||||
this._verticalScrollOverflowView = verticalScrollOverflowView;
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return {
|
||||
width: SCROLL_BAR_SIZE,
|
||||
height: 0, // No desired height
|
||||
};
|
||||
}
|
||||
|
||||
getMaxScrollThumbY(): number {
|
||||
const {height} = this.frame.size;
|
||||
|
||||
const maxScrollThumbY = height - this._scrollThumbRect.size.height;
|
||||
|
||||
return maxScrollThumbY;
|
||||
}
|
||||
|
||||
setContentHeight(contentHeight: number) {
|
||||
this._contentHeight = contentHeight;
|
||||
|
||||
const {height, width} = this.frame.size;
|
||||
|
||||
const proposedScrollThumbRect = {
|
||||
origin: {
|
||||
x: this.frame.origin.x,
|
||||
y: this._scrollThumbRect.origin.y,
|
||||
},
|
||||
size: {
|
||||
width,
|
||||
height: height * (height / contentHeight),
|
||||
},
|
||||
};
|
||||
|
||||
if (!rectEqualToRect(this._scrollThumbRect, proposedScrollThumbRect)) {
|
||||
this._scrollThumbRect = proposedScrollThumbRect;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
setScrollThumbY(value: number) {
|
||||
const {height} = this.frame.size;
|
||||
|
||||
const maxScrollThumbY = this.getMaxScrollThumbY();
|
||||
const newScrollThumbY = Math.max(0, Math.min(maxScrollThumbY, value));
|
||||
|
||||
this._scrollThumbRect = {
|
||||
...this._scrollThumbRect,
|
||||
origin: {
|
||||
x: this.frame.origin.x,
|
||||
y: newScrollThumbY,
|
||||
},
|
||||
};
|
||||
|
||||
const maxContentOffset = this._contentHeight - height;
|
||||
const contentScrollOffset =
|
||||
(newScrollThumbY / maxScrollThumbY) * maxContentOffset * -1;
|
||||
|
||||
this._verticalScrollOverflowView.setScrollOffset(
|
||||
contentScrollOffset,
|
||||
maxScrollThumbY,
|
||||
);
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
|
||||
const {x, y} = this.frame.origin;
|
||||
const {width, height} = this.frame.size;
|
||||
|
||||
// TODO Use real color
|
||||
context.fillStyle = COLORS.REACT_RESIZE_BAR;
|
||||
context.fillRect(x, y, width, height);
|
||||
|
||||
// TODO Use real color
|
||||
context.fillStyle = COLORS.SCROLL_CARET;
|
||||
context.fillRect(
|
||||
this._scrollThumbRect.origin.x,
|
||||
this._scrollThumbRect.origin.y,
|
||||
this._scrollThumbRect.size.width,
|
||||
this._scrollThumbRect.size.height,
|
||||
);
|
||||
|
||||
// TODO Use real color
|
||||
context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER;
|
||||
context.fillRect(x, y, BORDER_SIZE, height);
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'click':
|
||||
this._handleClick(interaction, viewRefs);
|
||||
break;
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction, viewRefs);
|
||||
break;
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction, viewRefs);
|
||||
break;
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction, viewRefs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_handleClick(interaction: ClickInteraction, viewRefs: ViewRefs) {
|
||||
const {location} = interaction.payload;
|
||||
if (rectContainsPoint(location, this.frame)) {
|
||||
if (rectContainsPoint(location, this._scrollThumbRect)) {
|
||||
// Ignore clicks on the track thumb directly.
|
||||
return;
|
||||
}
|
||||
|
||||
const currentScrollThumbY = this._scrollThumbRect.origin.y;
|
||||
const y = location.y;
|
||||
|
||||
const {height} = this.frame.size;
|
||||
|
||||
// Scroll up or down about one viewport worth of content:
|
||||
const deltaY = (height / this._contentHeight) * height * 0.8;
|
||||
|
||||
this.setScrollThumbY(
|
||||
y > currentScrollThumbY
|
||||
? this._scrollThumbRect.origin.y + deltaY
|
||||
: this._scrollThumbRect.origin.y - deltaY,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
|
||||
const {location} = interaction.payload;
|
||||
if (!rectContainsPoint(location, this._scrollThumbRect)) {
|
||||
return;
|
||||
}
|
||||
viewRefs.activeView = this;
|
||||
|
||||
this.currentCursor = 'default';
|
||||
|
||||
this._isScrolling = true;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
|
||||
const {event, location} = interaction.payload;
|
||||
if (rectContainsPoint(location, this.frame)) {
|
||||
if (viewRefs.hoveredView !== this) {
|
||||
viewRefs.hoveredView = this;
|
||||
}
|
||||
|
||||
this.currentCursor = 'default';
|
||||
}
|
||||
|
||||
if (viewRefs.activeView === this) {
|
||||
this.currentCursor = 'default';
|
||||
|
||||
this.setScrollThumbY(this._scrollThumbRect.origin.y + event.movementY);
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
|
||||
if (viewRefs.activeView === this) {
|
||||
viewRefs.activeView = null;
|
||||
}
|
||||
|
||||
if (this._isScrolling) {
|
||||
this._isScrolling = false;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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 {Rect} from '../geometry';
|
||||
import type {ScrollState} from '../utils/scrollState';
|
||||
import type {Surface} from '../Surface';
|
||||
import type {ViewState} from '../../types';
|
||||
|
||||
import {VerticalScrollBarView} from './VerticalScrollBarView';
|
||||
import {withVerticalScrollbarLayout} from './withVerticalScrollbarLayout';
|
||||
import {View} from '../View';
|
||||
import {VerticalScrollView} from '../VerticalScrollView';
|
||||
|
||||
export class VerticalScrollOverflowView extends View {
|
||||
_contentView: View;
|
||||
_isProcessingOnChange: boolean = false;
|
||||
_isScrolling: boolean = false;
|
||||
_scrollOffset: number = 0;
|
||||
_scrollBarView: VerticalScrollBarView;
|
||||
_verticalScrollView: VerticalScrollView;
|
||||
|
||||
constructor(
|
||||
surface: Surface,
|
||||
frame: Rect,
|
||||
contentView: View,
|
||||
viewState: ViewState,
|
||||
) {
|
||||
super(surface, frame, withVerticalScrollbarLayout);
|
||||
|
||||
this._contentView = contentView;
|
||||
this._verticalScrollView = new VerticalScrollView(
|
||||
surface,
|
||||
frame,
|
||||
contentView,
|
||||
viewState,
|
||||
'VerticalScrollOverflowView',
|
||||
);
|
||||
this._verticalScrollView.onChange(this._onVerticalScrollViewChange);
|
||||
|
||||
this._scrollBarView = new VerticalScrollBarView(surface, frame, this);
|
||||
|
||||
this.addSubview(this._verticalScrollView);
|
||||
this.addSubview(this._scrollBarView);
|
||||
}
|
||||
|
||||
layoutSubviews() {
|
||||
super.layoutSubviews();
|
||||
|
||||
const contentSize = this._contentView.desiredSize();
|
||||
|
||||
// This should be done after calling super.layoutSubviews() – calling it
|
||||
// before somehow causes _contentView to need display on every mousemove
|
||||
// event when the scroll bar is shown.
|
||||
this._scrollBarView.setContentHeight(contentSize.height);
|
||||
}
|
||||
|
||||
setScrollOffset(newScrollOffset: number, maxScrollOffset: number) {
|
||||
const deltaY = newScrollOffset - this._scrollOffset;
|
||||
|
||||
if (!this._isProcessingOnChange) {
|
||||
this._verticalScrollView.scrollBy(-deltaY);
|
||||
}
|
||||
|
||||
this._scrollOffset = newScrollOffset;
|
||||
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
_onVerticalScrollViewChange = (
|
||||
scrollState: ScrollState,
|
||||
containerLength: number,
|
||||
) => {
|
||||
const maxOffset = scrollState.length - containerLength;
|
||||
if (maxOffset === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const percentage = Math.abs(scrollState.offset) / maxOffset;
|
||||
const maxScrollThumbY = this._scrollBarView.getMaxScrollThumbY();
|
||||
|
||||
this._isProcessingOnChange = true;
|
||||
this._scrollBarView.setScrollThumbY(percentage * maxScrollThumbY);
|
||||
this._isProcessingOnChange = false;
|
||||
};
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export * from './VerticalScrollBarView';
|
||||
export * from './VerticalScrollOverflowView';
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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 {Layouter} from '../layouter';
|
||||
|
||||
/**
|
||||
* Assumes {@param layout} will only contain 2 views.
|
||||
*/
|
||||
export const withVerticalScrollbarLayout: Layouter = (
|
||||
layout,
|
||||
containerFrame,
|
||||
) => {
|
||||
const [contentLayoutInfo, scrollbarLayoutInfo] = layout;
|
||||
|
||||
const desiredContentSize = contentLayoutInfo.view.desiredSize();
|
||||
const shouldShowScrollbar =
|
||||
desiredContentSize.height > containerFrame.size.height;
|
||||
const scrollbarWidth = shouldShowScrollbar
|
||||
? scrollbarLayoutInfo.view.desiredSize().width
|
||||
: 0;
|
||||
|
||||
const laidOutContentLayoutInfo = {
|
||||
...contentLayoutInfo,
|
||||
frame: {
|
||||
origin: contentLayoutInfo.view.frame.origin,
|
||||
size: {
|
||||
width: containerFrame.size.width - scrollbarWidth,
|
||||
height: containerFrame.size.height,
|
||||
},
|
||||
},
|
||||
};
|
||||
const laidOutScrollbarLayoutInfo = {
|
||||
...scrollbarLayoutInfo,
|
||||
frame: {
|
||||
origin: {
|
||||
x:
|
||||
laidOutContentLayoutInfo.frame.origin.x +
|
||||
laidOutContentLayoutInfo.frame.size.width,
|
||||
y: containerFrame.origin.y,
|
||||
},
|
||||
size: {
|
||||
width: scrollbarWidth,
|
||||
height: containerFrame.size.height,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return [laidOutContentLayoutInfo, laidOutScrollbarLayoutInfo];
|
||||
};
|
||||
Reference in New Issue
Block a user