/**
* 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 {Rect} from '../geometry';
import type {Surface, ViewRefs} from '../Surface';
import type {
Interaction,
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(): {+height: number, +width: number} {
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();
}
}
}