/* eslint-disable react/display-name */ import * as React from "react"; import DrilldownOverlayEditor from "../internal/data-grid-overlay-editor/private/drilldown-overlay-editor.js"; import { getEmHeight, getMiddleCenterBias, measureTextCached, roundedRect, } from "../internal/data-grid/render/data-grid-lib.js"; import { GridCellKind, type DrilldownCell, type DrilldownCellData } from "../internal/data-grid/data-grid-types.js"; import type { BaseDrawArgs, InternalCellRenderer } from "./cell-types.js"; import { makeAccessibilityStringForArray } from "../common/utils.js"; export const drilldownCellRenderer: InternalCellRenderer = { getAccessibilityString: c => makeAccessibilityStringForArray(c.data.map(d => d.text)), kind: GridCellKind.Drilldown, needsHover: false, useLabel: false, needsHoverPosition: false, measure: (ctx, cell, t) => cell.data.reduce( (acc, data) => ctx.measureText(data.text).width + acc + 20 + (data.img !== undefined ? 18 : 0), 0 ) + 2 * t.cellHorizontalPadding - 4, draw: a => drawDrilldownCell(a, a.cell.data), provideEditor: () => p => { const { value } = p; return ; }, onPaste: () => undefined, }; const itemMargin = 4; const drilldownCache: { [key: string]: HTMLCanvasElement; } = {}; function getAndCacheDrilldownBorder( bgCell: string, border: string, height: number, rounding: number ): { el: HTMLCanvasElement; height: number; width: number; middleWidth: number; sideWidth: number; dpr: number; padding: number; } | null { const dpr = Math.ceil(window.devicePixelRatio); const shadowBlur = 5; const targetHeight = height - shadowBlur * 2; const middleWidth = 4; const innerHeight = height * dpr; const sideWidth = rounding + shadowBlur; const targetWidth = rounding * 3; const innerWidth = (targetWidth + shadowBlur * 2) * dpr; const key = `${bgCell},${border},${dpr},${height}`; if (drilldownCache[key] !== undefined) { return { el: drilldownCache[key], height: innerHeight, width: innerWidth, middleWidth: middleWidth * dpr, sideWidth: sideWidth * dpr, padding: shadowBlur * dpr, dpr, }; } const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); // alpha needed if (ctx === null) return null; canvas.width = innerWidth; canvas.height = innerHeight; ctx.scale(dpr, dpr); drilldownCache[key] = canvas; ctx.beginPath(); roundedRect(ctx, shadowBlur, shadowBlur, targetWidth, targetHeight, rounding); ctx.shadowColor = "rgba(24, 25, 34, 0.4)"; ctx.shadowBlur = 1; ctx.fillStyle = bgCell; ctx.fill(); ctx.shadowColor = "rgba(24, 25, 34, 0.3)"; ctx.shadowOffsetY = 1; ctx.shadowBlur = 5; ctx.fillStyle = bgCell; ctx.fill(); ctx.shadowOffsetY = 0; ctx.shadowBlur = 0; ctx.shadowBlur = 0; ctx.beginPath(); roundedRect(ctx, shadowBlur + 0.5, shadowBlur + 0.5, targetWidth, targetHeight, rounding); ctx.strokeStyle = border; ctx.lineWidth = 1; ctx.stroke(); return { el: canvas, height: innerHeight, width: innerWidth, sideWidth: sideWidth * dpr, middleWidth: rounding * dpr, padding: shadowBlur * dpr, dpr, }; } function drawDrilldownCell(args: BaseDrawArgs, data: readonly DrilldownCellData[]) { const { rect, theme, ctx, imageLoader, col, row } = args; const { x, width: w } = rect; const font = theme.baseFontFull; const emHeight = getEmHeight(ctx, font); const h = Math.min(rect.height, Math.max(16, Math.ceil(emHeight * theme.lineHeight) * 2)); const y = Math.floor(rect.y + (rect.height - h) / 2); const bubbleHeight = h - 10; const bubblePad = 8; const bubbleMargin = itemMargin; let renderX = x + theme.cellHorizontalPadding; const rounding = theme.roundingRadius ?? 6; const tileMap = getAndCacheDrilldownBorder(theme.bgCell, theme.drilldownBorder, h, rounding); const renderBoxes: { x: number; width: number }[] = []; for (const el of data) { if (renderX > x + w) break; const textMetrics = measureTextCached(el.text, ctx, font); const textWidth = textMetrics.width; let imgWidth = 0; if (el.img !== undefined) { const img = imageLoader.loadOrGetImage(el.img, col, row); if (img !== undefined) { imgWidth = bubbleHeight - 8 + 4; } } const renderWidth = textWidth + imgWidth + bubblePad * 2; renderBoxes.push({ x: renderX, width: renderWidth, }); renderX += renderWidth + bubbleMargin; } if (tileMap !== null) { const { el, height, middleWidth, sideWidth, width, dpr, padding } = tileMap; const outerSideWidth = sideWidth / dpr; const outerPadding = padding / dpr; for (const rectInfo of renderBoxes) { const rx = Math.floor(rectInfo.x); const rw = Math.floor(rectInfo.width); const outerMiddleWidth = rw - (outerSideWidth - outerPadding) * 2; ctx.imageSmoothingEnabled = false; ctx.drawImage(el, 0, 0, sideWidth, height, rx - outerPadding, y, outerSideWidth, h); if (outerMiddleWidth > 0) ctx.drawImage( el, sideWidth, 0, middleWidth, height, rx + (outerSideWidth - outerPadding), y, outerMiddleWidth, h ); ctx.drawImage( el, width - sideWidth, 0, sideWidth, height, rx + rw - (outerSideWidth - outerPadding), y, outerSideWidth, h ); ctx.imageSmoothingEnabled = true; } } ctx.beginPath(); for (const [i, rectInfo] of renderBoxes.entries()) { const d = data[i]; let drawX = rectInfo.x + bubblePad; if (d.img !== undefined) { const img = imageLoader.loadOrGetImage(d.img, col, row); if (img !== undefined) { const imgSize = bubbleHeight - 8; let srcX = 0; let srcY = 0; let srcWidth = img.width; let srcHeight = img.height; if (srcWidth > srcHeight) { // landscape srcX += (srcWidth - srcHeight) / 2; srcWidth = srcHeight; } else if (srcHeight > srcWidth) { //portrait srcY += (srcHeight - srcWidth) / 2; srcHeight = srcWidth; } ctx.beginPath(); roundedRect(ctx, drawX, y + h / 2 - imgSize / 2, imgSize, imgSize, theme.roundingRadius ?? 3); ctx.save(); ctx.clip(); ctx.drawImage(img, srcX, srcY, srcWidth, srcHeight, drawX, y + h / 2 - imgSize / 2, imgSize, imgSize); ctx.restore(); drawX += imgSize + 4; } } ctx.beginPath(); ctx.fillStyle = theme.textBubble; ctx.fillText(d.text, drawX, y + h / 2 + getMiddleCenterBias(ctx, theme)); } }