diff --git a/web/utils/text-utils.js b/web/utils/text-utils.js index 4af9be9f8..a1e405128 100644 --- a/web/utils/text-utils.js +++ b/web/utils/text-utils.js @@ -1,61 +1,65 @@ // @flow import invariant from 'invariant'; let canvas; function calculateMaxTextWidth( texts: $ReadOnlyArray, - font: string, + fontSize: number, ): number { + const font = + `${fontSize}px "Inter", -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", ` + + '"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", ui-sans-serif'; + if (!canvas) { canvas = document.createElement('canvas'); } const context = canvas.getContext('2d'); context.font = font; const widths = texts.map(text => context.measureText(text).width); return Math.max(...widths); } const numberOfPossibleByteValues = 256; function generateRandomString(length: number, availableSigns: string): string { invariant(length >= 0, 'length must be non-negative'); invariant( availableSigns !== '' || length === 0, "cannot create a random string of non-zero length from availableSigns = ''", ); invariant( numberOfPossibleByteValues >= availableSigns.length, `The number of available signs must not exceed ${numberOfPossibleByteValues}`, ); const validByteUpperBound = availableSigns.length * Math.floor(numberOfPossibleByteValues / availableSigns.length); // Generating more bytes than the required length, // proportionally to how many values will be omitted // due to uniformness requirement, // to lower the chances of having to draw again const drawBytes = Math.floor( length * (1 + 2 * (1 - validByteUpperBound / numberOfPossibleByteValues)), ); let str = ''; while (str.length < length) { const rand = new Uint8Array(drawBytes); crypto.getRandomValues(rand); for (let i = 0; str.length < length && i < drawBytes; i++) { if (rand[i] < validByteUpperBound) { const index = rand[i] % availableSigns.length; str += availableSigns.charAt(index); } } } return str; } export { calculateMaxTextWidth, generateRandomString }; diff --git a/web/utils/tooltip-utils.js b/web/utils/tooltip-utils.js index cef0b52d6..52646afd4 100644 --- a/web/utils/tooltip-utils.js +++ b/web/utils/tooltip-utils.js @@ -1,359 +1,355 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { getAppContainerPositionInfo } from './window-utils.js'; import { tooltipButtonStyle, tooltipLabelStyle, tooltipStyle, } from '../chat/chat-constants.js'; import type { PositionInfo } from '../chat/position-types.js'; import { calculateMaxTextWidth } from '../utils/text-utils.js'; export const tooltipPositions = Object.freeze({ LEFT: 'left', RIGHT: 'right', LEFT_BOTTOM: 'left-bottom', RIGHT_BOTTOM: 'right-bottom', LEFT_TOP: 'left-top', RIGHT_TOP: 'right-top', TOP: 'top', BOTTOM: 'bottom', }); export type TooltipSize = { +height: number, +width: number, }; export type TooltipPositionStyle = { +anchorPoint: { +x: number, +y: number, }, +verticalPosition: 'top' | 'bottom', +horizontalPosition: 'left' | 'right', +alignment: 'left' | 'center' | 'right', }; export type TooltipPosition = $Values; export type MessageTooltipAction = { +label: string, +onClick: (SyntheticEvent) => mixed, +actionButtonContent: React.Node, }; -const font = - '14px "Inter", -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", ' + - '"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", ui-sans-serif'; - type FindTooltipPositionArgs = { +sourcePositionInfo: PositionInfo, +tooltipSize: TooltipSize, +availablePositions: $ReadOnlyArray, +defaultPosition: TooltipPosition, +preventDisplayingBelowSource?: boolean, }; function findTooltipPosition({ sourcePositionInfo, tooltipSize, availablePositions, defaultPosition, preventDisplayingBelowSource, }: FindTooltipPositionArgs): TooltipPosition { const appContainerPositionInfo = getAppContainerPositionInfo(); if (!appContainerPositionInfo) { return defaultPosition; } const pointingTo = sourcePositionInfo; const { top: containerTop, left: containerLeft, right: containerRight, bottom: containerBottom, } = appContainerPositionInfo; const tooltipWidth = tooltipSize.width; const tooltipHeight = tooltipSize.height; const canBeDisplayedOnLeft = containerLeft + tooltipWidth <= pointingTo.left; const canBeDisplayedOnRight = tooltipWidth + pointingTo.right <= containerRight; const willCoverSidebarOnTopSideways = preventDisplayingBelowSource && pointingTo.top + tooltipHeight > pointingTo.bottom; const canBeDisplayedOnTopSideways = pointingTo.top >= containerTop && pointingTo.top + tooltipHeight <= containerBottom && !willCoverSidebarOnTopSideways; const canBeDisplayedOnBottomSideways = pointingTo.bottom <= containerBottom && pointingTo.bottom - tooltipHeight >= containerTop; const verticalCenterOfPointingTo = pointingTo.top + pointingTo.height / 2; const horizontalCenterOfPointingTo = pointingTo.left + pointingTo.width / 2; const willCoverSidebarInTheMiddleSideways = preventDisplayingBelowSource && verticalCenterOfPointingTo + tooltipHeight / 2 > pointingTo.bottom; const canBeDisplayedInTheMiddleSideways = verticalCenterOfPointingTo - tooltipHeight / 2 >= containerTop && verticalCenterOfPointingTo + tooltipHeight / 2 <= containerBottom && !willCoverSidebarInTheMiddleSideways; const canBeDisplayedOnTop = pointingTo.top - tooltipHeight >= containerTop && horizontalCenterOfPointingTo - tooltipWidth / 2 >= containerLeft && horizontalCenterOfPointingTo + tooltipWidth / 2 <= containerRight; const canBeDisplayedOnBottom = pointingTo.bottom + tooltipHeight <= containerBottom && horizontalCenterOfPointingTo - tooltipWidth / 2 >= containerLeft && horizontalCenterOfPointingTo + tooltipWidth / 2 <= containerRight && !preventDisplayingBelowSource; for (const tooltipPosition of availablePositions) { if ( tooltipPosition === tooltipPositions.RIGHT && canBeDisplayedOnRight && canBeDisplayedInTheMiddleSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.RIGHT_BOTTOM && canBeDisplayedOnRight && canBeDisplayedOnBottomSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.LEFT && canBeDisplayedOnLeft && canBeDisplayedInTheMiddleSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.LEFT_BOTTOM && canBeDisplayedOnLeft && canBeDisplayedOnBottomSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.LEFT_TOP && canBeDisplayedOnLeft && canBeDisplayedOnTopSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.RIGHT_TOP && canBeDisplayedOnRight && canBeDisplayedOnTopSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.TOP && canBeDisplayedOnTop ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.BOTTOM && canBeDisplayedOnBottom ) { return tooltipPosition; } } return defaultPosition; } type GetTooltipStyleParams = { +sourcePositionInfo: PositionInfo, +tooltipSize: TooltipSize, +tooltipPosition: TooltipPosition, }; // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return function getTooltipStyle({ sourcePositionInfo, tooltipSize, tooltipPosition, }: GetTooltipStyleParams): TooltipPositionStyle { if (tooltipPosition === tooltipPositions.RIGHT_TOP) { return { anchorPoint: { x: sourcePositionInfo.right, y: sourcePositionInfo.top, }, horizontalPosition: 'right', verticalPosition: 'bottom', alignment: 'left', }; } else if (tooltipPosition === tooltipPositions.LEFT_TOP) { return { anchorPoint: { x: sourcePositionInfo.left, y: sourcePositionInfo.top, }, horizontalPosition: 'left', verticalPosition: 'bottom', alignment: 'right', }; } else if (tooltipPosition === tooltipPositions.RIGHT_BOTTOM) { return { anchorPoint: { x: sourcePositionInfo.right, y: sourcePositionInfo.bottom, }, horizontalPosition: 'right', verticalPosition: 'top', alignment: 'left', }; } else if (tooltipPosition === tooltipPositions.LEFT_BOTTOM) { return { anchorPoint: { x: sourcePositionInfo.left, y: sourcePositionInfo.bottom, }, horizontalPosition: 'left', verticalPosition: 'top', alignment: 'right', }; } else if (tooltipPosition === tooltipPositions.LEFT) { return { anchorPoint: { x: sourcePositionInfo.left, y: sourcePositionInfo.top + sourcePositionInfo.height / 2 - tooltipSize.height / 2, }, horizontalPosition: 'left', verticalPosition: 'bottom', alignment: 'right', }; } else if (tooltipPosition === tooltipPositions.RIGHT) { return { anchorPoint: { x: sourcePositionInfo.right, y: sourcePositionInfo.top + sourcePositionInfo.height / 2 - tooltipSize.height / 2, }, horizontalPosition: 'right', verticalPosition: 'bottom', alignment: 'left', }; } else if (tooltipPosition === tooltipPositions.TOP) { return { anchorPoint: { x: sourcePositionInfo.left + sourcePositionInfo.width / 2 - tooltipSize.width / 2, y: sourcePositionInfo.top, }, horizontalPosition: 'right', verticalPosition: 'top', alignment: 'center', }; } else if (tooltipPosition === tooltipPositions.BOTTOM) { return { anchorPoint: { x: sourcePositionInfo.left + sourcePositionInfo.width / 2 - tooltipSize.width / 2, y: sourcePositionInfo.bottom, }, horizontalPosition: 'right', verticalPosition: 'bottom', alignment: 'center', }; } invariant(false, `Unexpected tooltip position value: ${tooltipPosition}`); } type GetTooltipPositionStyleParams = { +tooltipSourcePosition: ?PositionInfo, +tooltipSize: TooltipSize, +availablePositions: $ReadOnlyArray, +preventDisplayingBelowSource?: boolean, }; function getTooltipPositionStyle( params: GetTooltipPositionStyleParams, ): ?TooltipPositionStyle { const { tooltipSourcePosition, tooltipSize, availablePositions, preventDisplayingBelowSource, } = params; if (!tooltipSourcePosition) { return undefined; } const tooltipPosition = findTooltipPosition({ sourcePositionInfo: tooltipSourcePosition, tooltipSize, availablePositions, defaultPosition: availablePositions[0], preventDisplayingBelowSource, }); if (!tooltipPosition) { return undefined; } const tooltipPositionStyle = getTooltipStyle({ tooltipPosition, sourcePositionInfo: tooltipSourcePosition, tooltipSize, }); return tooltipPositionStyle; } type CalculateTooltipSizeArgs = { +tooltipLabels: $ReadOnlyArray, +timestamp: string, }; function calculateTooltipSize({ tooltipLabels, timestamp, }: CalculateTooltipSizeArgs): { +width: number, +height: number, } { const textWidth = - calculateMaxTextWidth([...tooltipLabels, timestamp], font) + + calculateMaxTextWidth([...tooltipLabels, timestamp], 14) + 2 * tooltipLabelStyle.padding; const buttonsWidth = tooltipLabels.length * (tooltipButtonStyle.width + tooltipButtonStyle.paddingLeft + tooltipButtonStyle.paddingRight); const width = Math.max(textWidth, buttonsWidth) + tooltipStyle.paddingLeft + tooltipStyle.paddingRight; const height = (tooltipLabelStyle.height + 2 * tooltipLabelStyle.padding) * 2 + tooltipStyle.rowGap * 2 + tooltipButtonStyle.height; return { width, height, }; } export { findTooltipPosition, getTooltipPositionStyle, calculateTooltipSize };