lazy-widgets
Version:
Typescript retained mode GUI for the HTML canvas API
690 lines • 29.6 kB
JavaScript
import { ViewportWidget } from './ViewportWidget.js';
import { AxisCoupling } from '../core/AxisCoupling.js';
import { PointerEvent } from '../events/PointerEvent.js';
import { PointerWheelEvent } from '../events/PointerWheelEvent.js';
import { ClickHelper } from '../helpers/ClickHelper.js';
import { ClickState } from '../helpers/ClickState.js';
import { TextHelper } from '../helpers/TextHelper.js';
import { AutoScrollEvent } from '../events/AutoScrollEvent.js';
import { LeaveEvent } from '../events/LeaveEvent.js';
import { PropagationModel } from '../events/WidgetEvent.js';
import { FocusEvent } from '../events/FocusEvent.js';
import { BlurEvent } from '../events/BlurEvent.js';
import { SingleParentXMLInputConfig } from '../xml/SingleParentXMLInputConfig.js';
import { safeRoundRect } from '../helpers/safeRoundRect.js';
import { Msg } from '../core/Strings.js';
/**
* The mode for how a scrollbar is shown in a {@link ScrollableViewportWidget}.
*
* @category Widget
*/
export var ScrollbarMode;
(function (ScrollbarMode) {
/** The scrollbar is an overlay and therefore only shown when needed */
ScrollbarMode[ScrollbarMode["Overlay"] = 0] = "Overlay";
/** The scrollbar is part of the layout and therefore always shown */
ScrollbarMode[ScrollbarMode["Layout"] = 1] = "Layout";
/** The scrollbar is hidden, but the content can still be scrolled */
ScrollbarMode[ScrollbarMode["Hidden"] = 2] = "Hidden";
})(ScrollbarMode || (ScrollbarMode = {}));
/**
* A wrapper for a {@link ViewportWidget} with scrollbars.
*
* Can be constrained to a specific type of children.
*
* If an axis is bi-coupled, that axis will not have a scrollbar.
*
* @category Widget
*/
export class ScrollableViewportWidget extends ViewportWidget {
constructor(child, properties) {
var _a;
super(child, properties);
/**
* The effective viewport width (ideal width not occupied by a non-overlay
* scrollbar), for scrollbar calculations. For internal use only.
*/
this.effectiveWidth = 0;
/**
* The effective viewport height (ideal height not occupied by a non-overlay
* scrollbar), for scrollbar calculations. For internal use only.
*/
this.effectiveHeight = 0;
/** Is the vertical scrollbar being dragged? If null, none is */
this.verticalDragged = null;
/** What was the starting scroll value before dragging? */
this.startingScroll = 0;
/** What was the normalised offset when starting drag? */
this.startingOffset = 0;
/**
* When was the last scroll attempt in milliseconds since Unix epoch? If 0,
* then there hasn't been a valid scroll recently.
*/
this.lastScroll = 0;
/** What was the child width on the last layout finalization? */
this.prevChildWidth = 0;
/** What was the child height on the last layout finalization? */
this.prevChildHeight = 0;
/** Was the horizontal scrollbar painted last frame? */
this.horizWasPainted = false;
/** Was the vertical scrollbar painted last frame? */
this.vertWasPainted = false;
/**
* The line height used for scrolling via wheel events.
*
* Should only be read from, instead of written. Use
* {@link ScrollableViewportWidget#updateScrollLineHeight} to update this
* value instead.
*/
this.scrollLineHeight = 0;
this._scrollbarMode = (_a = properties === null || properties === void 0 ? void 0 : properties.scrollbarMode) !== null && _a !== void 0 ? _a : ScrollbarMode.Overlay;
this.horizontalClickHelper = new ClickHelper(this);
this.verticalClickHelper = new ClickHelper(this);
this.updateScrollLineHeight();
}
/** The mode for how the scrollbar is shown. */
get scrollbarMode() {
return this._scrollbarMode;
}
set scrollbarMode(scrollbarMode) {
if (this._scrollbarMode !== scrollbarMode) {
const oldScroll = this.scroll;
this._scrollbarMode = scrollbarMode;
this.scroll = oldScroll;
this._layoutDirty = true;
this.markWholeAsDirty();
}
}
/**
* Offset of {@link SingleParent#child}. Positional events will take this
* into account, as well as rendering. Unlike {@link ViewportWidget#offset},
* this will clamp to possible scroll values to avoid issues.
*/
get offset() {
return super.offset;
}
set offset(offset) {
const [childWidth, childHeight] = this.child.idealDimensions;
super.offset = [
-Math.max(Math.min(-offset[0], childWidth - this.effectiveWidth), 0),
-Math.max(Math.min(-offset[1], childHeight - this.effectiveHeight), 0),
];
}
get widthCoupling() {
return super.widthCoupling;
}
set widthCoupling(widthCoupling) {
const oldScroll = this.scroll;
super.widthCoupling = widthCoupling;
this.scroll = oldScroll;
}
get heightCoupling() {
return super.heightCoupling;
}
set heightCoupling(heightCoupling) {
const oldScroll = this.scroll;
super.heightCoupling = heightCoupling;
this.scroll = oldScroll;
}
/**
* The current scroll values. Similar to
* {@link ScrollableViewportWidget#offset}, but with normalised values (from
* 0 to 1).
*/
get scroll() {
const [offsetX, offsetY] = this.offset;
const [childWidth, childHeight] = this.child.idealDimensions;
const diffX = childWidth - this.effectiveWidth;
const diffY = childHeight - this.effectiveHeight;
return [
diffX === 0 ? 0 : Math.min(Math.max(-offsetX / diffX, 0), 1),
diffY === 0 ? 0 : Math.min(Math.max(-offsetY / diffY, 0), 1),
];
}
set scroll(scroll) {
const [childWidth, childHeight] = this.child.idealDimensions;
this.offset = [
-scroll[0] * (childWidth - this.effectiveWidth),
-scroll[1] * (childHeight - this.effectiveHeight),
];
}
/** Get the ClickHelper of a scrollbar */
getClickHelper(vertical) {
if (vertical) {
return this.verticalClickHelper;
}
else {
return this.horizontalClickHelper;
}
}
/**
* Handle a pointer/leave event for a given scrollbar.
*
* @returns Returns true if the event was captured
*/
handleEventScrollbar(vertical, corner, event, root) {
// TODO repaint only scrollbars instead of everything if they just got
// hovered
// Abort if the other scrollbar is being dragged
if (this.verticalDragged !== null && this.verticalDragged !== vertical) {
return false;
}
// Get click area of scrollbar. If in overlay mode, use the filled part
// of the scrollbar as the click area since there is no background
const [fillRect, bgRect] = this.getScrollbarRects(vertical, corner);
const overlay = this._scrollbarMode === ScrollbarMode.Overlay;
const clickRect = overlay ? fillRect : bgRect;
const clickArea = [
clickRect[0],
clickRect[0] + clickRect[2],
clickRect[1],
clickRect[1] + clickRect[3],
];
// Handle click event
const clickHelper = this.getClickHelper(vertical);
clickHelper.handleClickEvent(event, root, clickArea);
const clickState = clickHelper.clickState;
const stateChanged = clickHelper.clickStateChanged;
if (stateChanged) {
this.markWholeAsDirty();
}
if (clickState === ClickState.Hold) {
// Abort if state is not valid, but grab the event
if (clickHelper.pointerPos === null || !(event instanceof PointerEvent)) {
return true;
}
const axisIndex = vertical ? 1 : 0;
const scroll = this.scroll;
// Skip check if in overlay mode; can only scroll by dragging in
// this mode
let inFilledArea = overlay;
if (!inFilledArea) {
inFilledArea = clickHelper.isPointInRect(event.x, event.y, fillRect[0], fillRect[0] + fillRect[2], fillRect[1], fillRect[1] + fillRect[3]);
}
// Find offset along scrollbar. Necessary for overlay mode since
// pointerPos is relative to the fillRect in that case, not bgRect
let thisOffset;
if (overlay) {
thisOffset = clickHelper.getNormalInRect(event.x, event.y, bgRect[0], bgRect[0] + bgRect[2], bgRect[1], bgRect[1] + bgRect[3])[axisIndex];
}
else {
thisOffset = clickHelper.pointerPos[axisIndex];
}
if (stateChanged) {
// If this was outside the filled area, snap scrollbar
if (!inFilledArea) {
const viewportLength = vertical ? this.effectiveHeight : this.effectiveWidth;
const childLength = this.child.idealDimensions[axisIndex];
const barLength = viewportLength / childLength;
scroll[axisIndex] = (thisOffset - barLength / 2) / (1 - barLength);
this.scroll = scroll;
}
// Drag start, save current scroll and set this scrollbar as
// being dragged
this.startingOffset = thisOffset;
this.startingScroll = scroll[axisIndex];
this.verticalDragged = vertical;
}
else {
// Drag continue, scroll
const viewportLength = vertical ? this.effectiveHeight : this.effectiveWidth;
const childLength = this.child.idealDimensions[axisIndex];
const barLength = viewportLength / childLength;
const dragDiff = thisOffset - this.startingOffset;
scroll[axisIndex] = this.startingScroll + dragDiff / (1 - barLength);
this.scroll = scroll;
}
return true;
}
else if (clickState === ClickState.Hover) {
return true;
}
else if (stateChanged) {
// Release this scrollbar
this.verticalDragged = null;
return false;
}
return false;
}
/** Clamp offset in-place to valid scroll values. For internal use only. */
clampOffset(offset) {
const [childWidth, childHeight] = this.child.idealDimensions;
const minX = -(childWidth - this.effectiveWidth);
if (minX >= 0) {
offset[0] = 0;
}
else if (offset[0] < minX) {
offset[0] = minX;
}
const minY = -(childHeight - this.effectiveHeight);
if (minY >= 0) {
offset[1] = 0;
}
else if (offset[1] < minY) {
offset[1] = minY;
}
}
/**
* Handle a wheel scroll event. If scrolling fails due to being at the
* limit, this returns true if the last scroll attempt happened less than
* 200 milliseconds ago.
*
* @returns Returns true if this changed scroll was successful
*/
handleWheelEvent(event) {
const offset = this.offset;
const [oldX, oldY] = offset;
const [dx, dy] = event.getDeltaPixels(true, this.scrollLineHeight, this.idealWidth, this.idealHeight);
offset[0] -= event.shift ? dy : dx;
offset[1] -= event.shift ? dx : dy;
this.clampOffset(offset);
this.offset = offset;
const [newX, newY] = this.offset;
const success = newX !== oldX || newY !== oldY;
const last = this.lastScroll;
if (success) {
this.lastScroll = (new Date()).getTime();
return true;
}
if (this.lastScroll === 0) {
return false;
}
else {
const now = (new Date()).getTime();
const elapsed = now - last;
if (elapsed < 200) {
this.lastScroll = now;
return true;
}
else {
return false;
}
}
}
updateScrollLineHeight() {
const textHelper = new TextHelper();
textHelper.font = this.bodyTextFont;
textHelper.lineHeight = this.bodyTextHeight;
textHelper.lineSpacing = this.bodyTextSpacing;
this.scrollLineHeight = textHelper.fullLineHeight;
}
onThemeUpdated(property = null) {
super.onThemeUpdated(property);
if (property === null || property === 'scrollBarThickness') {
this._layoutDirty = true;
this.markWholeAsDirty();
}
else if (property === 'bodyTextFont' ||
property === 'bodyTextHeight' ||
property === 'bodyTextSpacing') {
this.updateScrollLineHeight();
}
else if (property === 'backgroundFill' ||
property === 'scrollBarMinPercent' ||
property === 'scrollBarMinPixels' ||
property === 'primaryFill' ||
property === 'accentFill' ||
property === 'backgroundGlowFill') {
this.markWholeAsDirty();
}
}
handleEvent(event) {
if (event.propagation !== PropagationModel.Trickling) {
if (event.isa(FocusEvent) || event.isa(BlurEvent)) {
return this;
}
else {
return super.handleEvent(event);
}
}
// Try to drag a scrollbar if this is a pointer or leave event with no
// target or target on this. Don't do this if the scrollbars are hidden
const widthBiCoupled = this.widthCoupling === AxisCoupling.Bi;
const heightBiCoupled = this.heightCoupling === AxisCoupling.Bi;
if (this._scrollbarMode !== ScrollbarMode.Hidden &&
(event.isa(LeaveEvent) || event instanceof PointerEvent) &&
(event.target === null || event.target === this)) {
const [childWidth, childHeight] = this.child.idealDimensions;
const overlay = this._scrollbarMode === ScrollbarMode.Overlay;
const forceCorner = !overlay && (!widthBiCoupled && !heightBiCoupled);
const xNeeded = childWidth > this.idealWidth;
const yNeeded = childHeight > this.idealHeight;
let grabbedEvent = false;
// Only handle event in scrollbar if the scrollbar is shown and
// needed (layout mode shows unneeded scrollbars)
if (!widthBiCoupled && (xNeeded || !overlay) &&
this.handleEventScrollbar(false, yNeeded || forceCorner, event, this.root)) {
grabbedEvent = true;
}
if (!heightBiCoupled && (yNeeded || !overlay) &&
this.handleEventScrollbar(true, xNeeded || forceCorner, event, this.root)) {
grabbedEvent = true;
}
// If the event was grabbed by either scrollbar, capture it
if (grabbedEvent) {
// If this is a wheel event, handle it
if (event.isa(PointerWheelEvent)) {
this.handleWheelEvent(event);
}
return this;
}
}
// Pass event along
const capturer = super.handleEvent(event);
// If this is an auto-scroll event and it's been captured, then scroll
// to the capturer's wanted bounds, make the event relative to this
// scrollable viewport and re-capture it
if (capturer !== null && event.isa(AutoScrollEvent)) {
const reserve = this._scrollbarMode === ScrollbarMode.Layout;
const reserveX = reserve && !heightBiCoupled;
const reserveY = reserve && !widthBiCoupled;
let clearWidth = this.effectiveWidth;
let clearHeight = this.effectiveHeight;
if (!reserveX || !reserveY) {
const thickness = this.scrollBarThickness;
const [childWidth, childHeight] = this.child.idealDimensions;
const xNeeded = childWidth > this.idealWidth;
const yNeeded = childHeight > this.idealHeight;
const paintX = this.scrollbarNeedsPaint(false, xNeeded);
const paintY = this.scrollbarNeedsPaint(true, yNeeded);
// XXX don't trim clear space if scrollbars are hidden
if (this._scrollbarMode !== ScrollbarMode.Hidden) {
if (!reserveX && paintY) {
clearWidth = Math.max(0, clearWidth - thickness);
}
if (!reserveY && paintX) {
clearHeight = Math.max(0, clearHeight - thickness);
}
}
}
let [cx, cy] = capturer.idealPosition;
// XXX if a viewport is being used, then the child's coordinates are
// relative to the viewport widget. convert coordinates so that they
// are relative to the viewport widget's parent viewport
let [offsetX, offsetY] = this.offset;
const oldOffX = offsetX, oldOffY = offsetY;
if (this.internalViewport.relativeCoordinates) {
cx += this.idealX + offsetX;
cy += this.idealY + offsetY;
}
let [cl, cr, ct, cb] = event.bounds;
cl += cx;
cr += cx;
ct += cy;
cb += cy;
const vpr = this.idealX + clearWidth;
const vpb = this.idealY + clearHeight;
// If a tab-selection event occurred, scroll so that widget that got
// selected is visible. Don't scroll if viewport is smaller than
// capturer and viewport is inside capturer. Don't scroll if
// capturer is smaller than viewport and capturer is inside viewport
const moveX = !widthBiCoupled && !(cl >= this.idealX && cr <= vpr) && !(this.idealX >= cl && vpr <= cr);
if (moveX) {
// If child rect is bigger than viewport, then align nearest
// child rect edge to farthest border of viewport
// If child rect is smaller than viewport, then align farthest
// child rect edge to nearest border of viewport
const rectBiggerThanViewport = cr - cl > clearWidth;
const rectBeforeViewport = cl < this.idealX;
const alignLeft = rectBiggerThanViewport !== rectBeforeViewport;
if (alignLeft) {
offsetX += this.idealX - cl;
}
else {
offsetX += vpr - cr;
}
}
const moveY = !heightBiCoupled && !(ct >= this.idealY && cb <= vpb) && !(this.idealY >= ct && vpb <= cb);
if (moveY) {
const rectBiggerThanViewport = cb - ct > clearHeight;
const rectBeforeViewport = ct < this.idealY;
const alignTop = rectBiggerThanViewport !== rectBeforeViewport;
if (alignTop) {
offsetY += this.idealY - ct;
}
else {
offsetY += vpb - cb;
}
}
if (moveX || moveY) {
this.offset = [offsetX, offsetY];
}
// Correct event bounds to new offset
// XXX need to use getter instead of [offsetX, offsetY] because the
// setter clamps the values and therefore the offset may have
// changed
const [newOffX, newOffY] = this.offset;
const offDiffX = newOffX - oldOffX + cx - this.idealX;
const offDiffY = newOffY - oldOffY + cy - this.idealY;
event.bounds[0] += offDiffX;
event.bounds[1] += offDiffX;
event.bounds[2] += offDiffY;
event.bounds[3] += offDiffY;
return this;
}
// If this is a wheel event and nobody captured the event, try
// scrolling. If scrolling did indeed occur, then capture the event.
if (capturer === null && event.isa(PointerWheelEvent) && this.handleWheelEvent(event)) {
return this;
}
return capturer;
}
handleResolveDimensions(minWidth, maxWidth, minHeight, maxHeight) {
// Reserve space for scrollbars if needed
const thickness = this.scrollBarThickness;
const reserve = this._scrollbarMode === ScrollbarMode.Layout;
this.reservedX = reserve && this.heightCoupling !== AxisCoupling.Bi ? thickness : 0;
this.reservedY = reserve && this.widthCoupling !== AxisCoupling.Bi ? thickness : 0;
// Resolve dimensions
super.handleResolveDimensions(minWidth, maxWidth, minHeight, maxHeight);
// Expand dimensions to fit scrollbars
this.idealWidth = Math.min(Math.max(this.idealWidth + this.reservedX, minWidth), maxWidth);
this.idealHeight = Math.min(Math.max(this.idealHeight + this.reservedY, minHeight), maxHeight);
}
finalizeBounds() {
super.finalizeBounds();
// Mark as dirty if scrollbars changed (check this by comparing old dims
// with new dims of child viewport)
// Get dimensions of child viewport before resolving them
let scrollbarsDirty = false;
const [newChildWidth, newChildHeight] = this.child.idealDimensions;
const newPaintX = this.scrollbarNeedsPaint(false, newChildWidth > this.effectiveWidth);
if ((this.horizWasPainted !== newPaintX) || (newPaintX && newChildWidth !== this.prevChildWidth)) {
scrollbarsDirty = true;
}
else {
const newPaintY = this.scrollbarNeedsPaint(true, newChildHeight > this.effectiveHeight);
scrollbarsDirty = (this.vertWasPainted !== newPaintY) || (newPaintY && newChildHeight !== this.prevChildHeight);
}
this.prevChildWidth = newChildWidth;
this.prevChildHeight = newChildHeight;
if (scrollbarsDirty) {
this.markWholeAsDirty();
}
// Save dimensions to effective dimensions
this.effectiveWidth = Math.max(0, this.width - this.reservedX);
this.effectiveHeight = Math.max(0, this.height - this.reservedY);
}
handlePostLayoutUpdate() {
super.handlePostLayoutUpdate();
// Keep scroll in bounds
const offset = this.offset;
this.clampOffset(offset);
this.offset = offset;
}
handlePreLayoutUpdate() {
super.handlePreLayoutUpdate();
this.horizontalClickHelper.doneProcessing();
this.verticalClickHelper.doneProcessing();
}
handlePainting(dirtyRects) {
// Check which scrollbars need painting
const [childWidth, childHeight] = this.child.idealDimensions;
const xNeeded = childWidth > this.effectiveWidth;
const yNeeded = childHeight > this.effectiveHeight;
const paintX = this.scrollbarNeedsPaint(false, xNeeded);
const paintY = this.scrollbarNeedsPaint(true, yNeeded);
this.horizWasPainted = paintX;
this.vertWasPainted = paintY;
// Paint viewport
super.handlePainting(dirtyRects);
// Paint scrollbars
const forceCorner = this._scrollbarMode === ScrollbarMode.Layout &&
(this.widthCoupling !== AxisCoupling.Bi
&& this.heightCoupling !== AxisCoupling.Bi);
if (paintX) {
this.paintScrollbar(false, xNeeded, yNeeded || forceCorner);
}
if (paintY) {
this.paintScrollbar(true, yNeeded, xNeeded || forceCorner);
}
// Paint corner if it is forced
if (forceCorner) {
const thickness = this.scrollBarThickness;
const ctx = this.viewport.context;
ctx.fillStyle = this.backgroundFill;
ctx.fillRect(this.x + this.width - thickness, this.y + this.height - thickness, thickness, thickness);
}
}
/**
* Get the rectangles (filled and background) of a scrollbar
*
* @returns Returns a 2-tuple with 2 4-tuples. The first one is the scrollbar fill rectangle and the second one is the background fill rectangle. Each rectangle 4-tuple contains, respectively, horizontal offset, vertical offset, width and height
*/
getScrollbarRects(vertical, corner) {
// Calculate basic scrollbar properties
const overlay = this._scrollbarMode === ScrollbarMode.Overlay;
const axisIndex = vertical ? 1 : 0;
const percent = this.scroll[axisIndex];
const childLength = this.child.idealDimensions[axisIndex];
const viewportLength = vertical ? this.effectiveHeight : this.effectiveWidth;
const thickness = Math.min(this.scrollBarThickness, this.width / 2, this.height / 2);
const minPercent = this.scrollBarMinPercent;
const minPixels = this.scrollBarMinPixels;
let viewportLengthCorner = viewportLength;
if (overlay) {
viewportLengthCorner -= (corner ? thickness : 0);
}
const length = Math.min(
// Make sure scrollbar fill isn't bigger than viewport
Math.max(
// Make sure that scrollbar respects the minimum pixel length
minPixels, Math.max(
// Make sure that scrollbar respects the minimum percent
viewportLength / childLength, minPercent) * viewportLengthCorner), viewportLength);
const offset = (viewportLengthCorner - length) * percent;
// Find rectangle where filled part of scrollbar will be painted
let sX, sY, sWidth, sHeight;
if (vertical) {
sX = this.idealX + this.idealWidth - thickness;
sY = this.idealY + offset;
sWidth = thickness;
sHeight = length;
}
else {
sX = this.idealX + offset;
sY = this.idealY + this.idealHeight - thickness;
sWidth = length;
sHeight = thickness;
}
// Find rectangle where background of scrollbar will be painted
let bgX, bgY, bgWidth, bgHeight;
if (vertical) {
bgX = sX;
bgY = this.idealY;
bgWidth = thickness;
bgHeight = viewportLengthCorner;
}
else {
bgX = this.idealX;
bgY = sY;
bgWidth = viewportLengthCorner;
bgHeight = thickness;
}
// Apply padding
const pad = this.scrollbarPadding;
if (vertical) {
sY += pad.top;
sHeight = Math.max(0, sHeight - pad.top - pad.bottom);
if (sHeight === 0) {
ScrollableViewportWidget.warnInvisiblePad();
}
}
else {
sX += pad.left;
sWidth = Math.max(0, sWidth - pad.left - pad.right);
if (sWidth === 0) {
ScrollableViewportWidget.warnInvisiblePad();
}
}
return [
[sX, sY, sWidth, sHeight],
[bgX, bgY, bgWidth, bgHeight],
];
}
/** Check if a scrollbar needs to be painted */
scrollbarNeedsPaint(vertical, needed) {
if (this._scrollbarMode === ScrollbarMode.Hidden) {
return false;
}
if (!needed && this._scrollbarMode === ScrollbarMode.Overlay) {
return false;
}
if (vertical) {
return this.heightCoupling !== AxisCoupling.Bi;
}
else {
return this.widthCoupling !== AxisCoupling.Bi;
}
}
/** Paint a scrollbar. For internal use only */
paintScrollbar(vertical, needed, corner) {
// Get rectangles
const [fillRect, bgRect] = this.getScrollbarRects(vertical, corner);
// Paint background if not in overlay mode
const ctx = this.viewport.context;
if (this._scrollbarMode !== ScrollbarMode.Overlay) {
ctx.fillStyle = this.backgroundFill;
ctx.fillRect(...bgRect);
}
// Paint filled part of scrollbar
if (needed) {
const clickHelper = this.getClickHelper(vertical);
switch (clickHelper.clickState) {
case ClickState.Released:
ctx.fillStyle = this.primaryFill;
break;
case ClickState.Hover:
case ClickState.Hold:
ctx.fillStyle = this.accentFill;
break;
}
}
else {
ctx.fillStyle = this.backgroundGlowFill;
}
const radii = this.scrollbarCornersRadii;
if (radii !== 0) {
ctx.save();
ctx.beginPath();
safeRoundRect(ctx, ...fillRect, radii);
ctx.clip();
}
ctx.fillRect(...fillRect);
if (radii !== 0) {
ctx.restore();
}
}
static warnInvisiblePad() {
if (!ScrollableViewportWidget.invisiblePadWarned) {
ScrollableViewportWidget.invisiblePadWarned = true;
console.warn(Msg.SCROLLBAR_INVIS_PAD);
}
}
}
ScrollableViewportWidget.autoXML = {
name: 'scrollable-viewport-widget',
inputConfig: SingleParentXMLInputConfig
};
ScrollableViewportWidget.invisiblePadWarned = false;
//# sourceMappingURL=ScrollableViewportWidget.js.map