cheetah-grid
Version:
Cheetah Grid is a high performance grid engine that works on canvas
1,846 lines (1,782 loc) • 48.7 kB
text/typescript
import * as calc from "./internal/calc";
import * as canvashelper from "./tools/canvashelper";
import * as fonts from "./internal/fonts";
import * as inlineUtils from "./element/inlines";
import * as themes from "./themes";
import type {
CellContext,
ColorDef,
ColorPropertyDefine,
ColorsPropertyDefine,
FontPropertyDefine,
GridCanvasHelperAPI,
LineClamp,
ListGridAPI,
RectProps,
RequiredThemeDefine,
StylePropertyFunctionArg,
TextOverflow,
TreeBranchIconStyleDefine,
TreeLineStyle,
} from "./ts-types";
import type { Inline, InlineDrawOption } from "./element/Inline";
import { calcStartPosition, getFontSize } from "./internal/canvases";
import {
cellEquals,
cellInRange,
getChainSafe,
getOrApply,
style,
} from "./internal/utils";
import { InlineDrawer } from "./element/InlineDrawer";
import type { PaddingOption } from "./internal/canvases";
import type { RGBA } from "./internal/color";
import { Rect } from "./internal/Rect";
import type { SimpleColumnIconOption } from "./ts-types-internal";
import { colorToRGB } from "./internal/color";
const { toBoxArray } = style;
const INLINE_ELLIPSIS = inlineUtils.of("\u2026");
const TEXT_OFFSET = 2;
const CHECKBOX_OFFSET = TEXT_OFFSET + 1;
type ColorsDef = ColorDef | (ColorDef | null)[];
function invalidateCell<T>(context: CellContext, grid: ListGridAPI<T>): void {
const { col, row } = context;
grid.invalidateCell(col, row);
}
function getStyleProperty<T>(
color: ColorPropertyDefine,
col: number,
row: number,
grid: ListGridAPI<T>,
context: CanvasRenderingContext2D
): ColorDef;
function getStyleProperty<T>(
color: ColorsPropertyDefine,
col: number,
row: number,
grid: ListGridAPI<T>,
context: CanvasRenderingContext2D
): ColorsDef;
function getStyleProperty<T>(
color: undefined,
col: number,
row: number,
grid: ListGridAPI<T>,
context: CanvasRenderingContext2D
): undefined;
function getStyleProperty<T, P>(
style: P | ((args: StylePropertyFunctionArg) => P),
col: number,
row: number,
grid: ListGridAPI<T>,
context: CanvasRenderingContext2D
): P;
function getStyleProperty<T, P>(
color:
| ColorPropertyDefine
| ColorsPropertyDefine
| undefined
| P
| ((args: StylePropertyFunctionArg) => P),
col: number,
row: number,
grid: ListGridAPI<T>,
context: CanvasRenderingContext2D
): ColorDef | ColorsDef | undefined | P {
return getOrApply(color, {
col,
row,
grid,
context,
});
}
function getFont<T>(
font: FontPropertyDefine | undefined,
col: number,
row: number,
grid: ListGridAPI<T>,
context: CanvasRenderingContext2D
): string | undefined {
if (font == null) {
return undefined;
}
return getOrApply(font, {
col,
row,
grid,
context,
});
}
function getThemeValue<R, T>(grid: ListGridAPI<R>, ...names: string[]): T {
const gridThemeValue = getChainSafe(grid.theme, ...names);
if (gridThemeValue == null) {
// use default theme
return getChainSafe(themes.getDefault(), ...names);
}
if (typeof gridThemeValue !== "function") {
return gridThemeValue;
}
let defaultThemeValue: unknown;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return ((args: StylePropertyFunctionArg): any => {
const value = gridThemeValue(args);
if (value != null) {
// use grid theme
return value;
}
// use default theme
defaultThemeValue =
defaultThemeValue || getChainSafe(themes.getDefault(), ...names);
return getOrApply(defaultThemeValue, args);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any;
}
function testFontLoad<T>(
font: string | undefined,
value: string,
context: CellContext,
grid: ListGridAPI<T>
): boolean {
if (font) {
if (!fonts.check(font, value)) {
fonts.load(font, value, () => invalidateCell(context, grid));
return false;
}
}
return true;
}
function drawInlines<T>(
ctx: CanvasRenderingContext2D,
inlines: Inline[],
rect: RectProps,
offset: number,
offsetTop: number,
offsetBottom: number,
col: number,
row: number,
grid: ListGridAPI<T>
): void {
function drawInline(
inline: Inline,
offsetLeft: number,
offsetRight: number
): void {
if (inline.canDraw()) {
ctx.save();
try {
ctx.fillStyle = getStyleProperty(
inline.color() || ctx.fillStyle,
col,
row,
grid,
ctx
);
ctx.font = inline.font() || ctx.font;
inline.draw({
ctx,
canvashelper,
rect,
offset,
offsetLeft,
offsetRight,
offsetTop,
offsetBottom,
});
} finally {
ctx.restore();
}
} else {
inline.onReady(() => grid.invalidateCell(col, row));
//noop
}
}
if (inlines.length === 1) {
//1件の場合は幅計算が不要なため分岐
const inline = inlines[0];
drawInline(inline, 0, 0);
} else {
const inlineWidths = inlines.map(
(inline) => (inline.width({ ctx }) || 0) - 0
);
let offsetRight = inlineWidths.reduce((a, b) => a + b);
let offsetLeft = 0;
inlines.forEach((inline, index) => {
const inlineWidth = inlineWidths[index];
offsetRight -= inlineWidth;
drawInline(inline, offsetLeft, offsetRight);
offsetLeft += inlineWidth;
});
}
}
function buildInlines(
icons: SimpleColumnIconOption[] | undefined,
inline: string | (Inline | string)[]
): Inline[] {
return inlineUtils.buildInlines(icons, inline || "");
}
function inlineToString(inline: Inline | string | (Inline | string)[]): string {
return inlineUtils.string(inline);
}
function getOverflowInline(textOverflow?: TextOverflow): Inline {
if (!isAllowOverflow(textOverflow) || textOverflow === "ellipsis") {
return INLINE_ELLIPSIS;
}
textOverflow = textOverflow.trim();
if (textOverflow.length === 1) {
return inlineUtils.of(textOverflow[0]);
}
return INLINE_ELLIPSIS;
}
function isAllowOverflow(textOverflow?: TextOverflow): textOverflow is string {
return Boolean(
textOverflow && textOverflow !== "clip" && typeof textOverflow === "string"
);
}
function getOverflowInlinesIndex(
ctx: CanvasRenderingContext2D,
inlines: Inline[],
width: number
): {
index: number;
lineWidth: number;
remWidth: number;
} | null {
const maxWidth = width - 3; /*buffer*/
let lineWidth = 0;
for (let i = 0; i < inlines.length; i++) {
const inline = inlines[i];
const inlineWidth = (inline.width({ ctx }) || 0) - 0;
if (lineWidth + inlineWidth > maxWidth) {
return {
index: i,
lineWidth,
remWidth: maxWidth - lineWidth,
};
}
lineWidth += inlineWidth;
}
return null;
}
function isOverflowInlines(
ctx: CanvasRenderingContext2D,
inlines: Inline[],
width: number
): boolean {
return !!getOverflowInlinesIndex(ctx, inlines, width);
}
function breakWidthInlines(
ctx: CanvasRenderingContext2D,
inlines: Inline[],
width: number
): {
beforeInlines: Inline[];
overflow: boolean;
afterInlines: Inline[];
} {
const indexData = getOverflowInlinesIndex(ctx, inlines, width);
if (!indexData) {
return {
beforeInlines: inlines,
overflow: false,
afterInlines: [],
};
}
const { index, remWidth } = indexData;
const inline = inlines[index];
const beforeInlines = inlines.slice(0, index);
const afterInlines = [];
if (inline.canBreak()) {
let { before, after } = inline.breakWord(ctx, remWidth);
if (!before && !beforeInlines.length) {
({ before, after } = inline.breakAll(ctx, remWidth));
}
if (!before && !beforeInlines.length) {
// Always return one char
({ before, after } = inline.splitIndex(1));
}
if (before) {
beforeInlines.push(before);
}
if (after) {
afterInlines.push(after);
}
afterInlines.push(...inlines.slice(index + 1));
} else {
if (!beforeInlines.length) {
// Always return one char
beforeInlines.push(inline);
}
afterInlines.push(...inlines.slice(beforeInlines.length));
}
return {
beforeInlines,
overflow: true,
afterInlines,
};
}
function truncateInlines(
ctx: CanvasRenderingContext2D,
inlines: Inline[],
width: number,
option?: TextOverflow
): {
inlines: Inline[];
overflow: boolean;
} {
const indexData = getOverflowInlinesIndex(ctx, inlines, width);
if (!indexData) {
return {
inlines,
overflow: false,
};
}
const { index, lineWidth } = indexData;
const inline = inlines[index];
const overflowInline = getOverflowInline(option);
const ellipsisWidth = overflowInline.width({ ctx });
const remWidth = width - lineWidth - ellipsisWidth;
const result = inlines.slice(0, index);
if (inline.canBreak()) {
const { before } = inline.breakAll(ctx, remWidth);
if (before) {
result.push(before);
}
}
result.push(overflowInline);
return {
inlines: result,
overflow: true,
};
}
function _inlineRect<T>(
grid: ListGridAPI<T>,
ctx: CanvasRenderingContext2D,
inline: string | (Inline | string)[],
drawRect: RectProps,
col: number,
row: number,
{
offset,
color,
textAlign,
textBaseline,
font,
textOverflow,
icons,
trailingIcon,
}: {
offset: number;
color?: ColorPropertyDefine;
textAlign: CanvasTextAlign;
textBaseline: CanvasTextBaseline;
font?: string;
textOverflow?: TextOverflow;
icons?: SimpleColumnIconOption[];
trailingIcon?: SimpleColumnIconOption;
}
): void {
//文字style
ctx.fillStyle = getStyleProperty(color || ctx.fillStyle, col, row, grid, ctx);
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = font || ctx.font;
let inlines = buildInlines(icons, inline);
const trailingIconInline = trailingIcon
? inlineUtils.iconOf(trailingIcon)
: null;
let inlineDrawRect = drawRect;
let { width } = drawRect;
let trailingIconWidth = 0;
if (trailingIconInline) {
trailingIconWidth = trailingIconInline.width({ ctx });
width -= trailingIconWidth;
inlineDrawRect = new Rect(
drawRect.left,
drawRect.top,
width,
drawRect.height
);
}
if (isAllowOverflow(textOverflow) && isOverflowInlines(ctx, inlines, width)) {
const { inlines: truncInlines, overflow } = truncateInlines(
ctx,
inlines,
width,
textOverflow
);
inlines = truncInlines;
grid.setCellOverflowText(col, row, overflow && inlineToString(inline));
} else {
grid.setCellOverflowText(col, row, false);
}
drawInlines(ctx, inlines, inlineDrawRect, offset, 0, 0, col, row, grid);
if (trailingIconInline) {
// Draw trailing icon
let sumWidth = 0;
inlines.forEach((inline) => {
sumWidth += inline.width({ ctx });
});
const baseRect = new Rect(
drawRect.left,
drawRect.top,
drawRect.width,
drawRect.height
);
const trailingIconRect = baseRect.copy();
if (width < sumWidth) {
trailingIconRect.left =
trailingIconRect.right - trailingIconWidth - offset;
} else {
trailingIconRect.left += sumWidth;
}
trailingIconRect.right = baseRect.right;
drawInlines(
ctx,
[trailingIconInline],
trailingIconRect,
offset,
0,
0,
col,
row,
grid
);
}
}
// eslint-disable-next-line complexity
function _multiInlineRect<T>(
grid: ListGridAPI<T>,
ctx: CanvasRenderingContext2D,
multiInlines: string[],
drawRect: RectProps,
col: number,
row: number,
{
offset,
color,
textAlign,
textBaseline,
font,
lineHeight,
autoWrapText,
lineClamp,
textOverflow,
icons,
trailingIcon,
}: {
offset: number;
color?: ColorPropertyDefine;
textAlign: CanvasTextAlign;
textBaseline: CanvasTextBaseline;
font?: string;
lineHeight: number;
autoWrapText?: boolean;
lineClamp: LineClamp;
textOverflow?: TextOverflow;
icons?: SimpleColumnIconOption[];
trailingIcon?: SimpleColumnIconOption;
}
): void {
//文字style
ctx.fillStyle = getStyleProperty(color || ctx.fillStyle, col, row, grid, ctx);
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = font || ctx.font;
if (lineClamp === "auto") {
const rectHeight =
drawRect.height - offset * 2 - 2; /*offset added by Inline#draw*/
lineClamp = Math.max(Math.floor(rectHeight / lineHeight), 1);
}
const trailingIconInline = trailingIcon
? inlineUtils.iconOf(trailingIcon)
: null;
let { width } = drawRect;
let trailingIconWidth = 0;
if (trailingIconInline) {
trailingIconWidth = trailingIconInline.width({ ctx });
width -= trailingIconWidth;
}
let buildedMultiInlines: Inline[][];
if (autoWrapText || lineClamp > 0 || isAllowOverflow(textOverflow)) {
buildedMultiInlines = [];
const procLineClamp =
lineClamp > 0
? (inlines: Inline[], hasNext: boolean): boolean => {
if (buildedMultiInlines.length + 1 >= lineClamp) {
if (inlines.length === 0 && hasNext) {
buildedMultiInlines.push([getOverflowInline(textOverflow)]);
grid.setCellOverflowText(
col,
row,
multiInlines.map(inlineToString).join("\n")
);
} else {
const { inlines: truncInlines, overflow } = truncateInlines(
ctx,
inlines,
width,
textOverflow
);
buildedMultiInlines.push(
hasNext && !overflow
? truncInlines.concat([getOverflowInline(textOverflow)])
: truncInlines
);
if (overflow || hasNext) {
grid.setCellOverflowText(
col,
row,
multiInlines.map(inlineToString).join("\n")
);
}
}
return false;
}
return true;
}
: (): boolean => true;
const procLine = autoWrapText
? (inlines: Inline[], hasNext: boolean): boolean => {
if (!procLineClamp(inlines, hasNext)) {
return false;
}
while (inlines.length) {
if (!procLineClamp(inlines, hasNext)) {
return false;
}
const { beforeInlines, afterInlines } = breakWidthInlines(
ctx,
inlines,
width
);
buildedMultiInlines.push(beforeInlines);
inlines = afterInlines;
}
return true;
}
: isAllowOverflow(textOverflow)
? (inlines: Inline[], hasNext: boolean): boolean => {
if (!procLineClamp(inlines, hasNext)) {
return false;
}
const { inlines: truncInlines, overflow } = truncateInlines(
ctx,
inlines,
width,
textOverflow
);
buildedMultiInlines.push(truncInlines);
if (overflow) {
grid.setCellOverflowText(
col,
row,
multiInlines.map(inlineToString).join("\n")
);
}
return true;
}
: (inlines: Inline[], hasNext: boolean): boolean => {
if (!procLineClamp(inlines, hasNext)) {
return false;
}
buildedMultiInlines.push(inlines);
return true;
};
grid.setCellOverflowText(col, row, false);
for (let lineRow = 0; lineRow < multiInlines.length; lineRow++) {
const inline = multiInlines[lineRow];
const buildedInline = buildInlines(
lineRow === 0 ? icons : undefined,
inline
);
if (!procLine(buildedInline, lineRow + 1 < multiInlines.length)) {
break;
}
}
} else {
grid.setCellOverflowText(col, row, false);
buildedMultiInlines = multiInlines.map((inline, lineRow) =>
buildInlines(lineRow === 0 ? icons : undefined, inline)
);
}
let paddingTop = 0;
let paddingBottom = lineHeight * (buildedMultiInlines.length - 1);
if (ctx.textBaseline === "top" || ctx.textBaseline === "hanging") {
const em = getFontSize(ctx, ctx.font).height;
const pad = (lineHeight - em) / 2;
paddingTop += pad;
paddingBottom -= pad;
} else if (
ctx.textBaseline === "bottom" ||
ctx.textBaseline === "alphabetic" ||
ctx.textBaseline === "ideographic"
) {
const em = getFontSize(ctx, ctx.font).height;
const pad = (lineHeight - em) / 2;
paddingTop -= pad;
paddingBottom += pad;
}
buildedMultiInlines.forEach((buildedInline) => {
drawInlines(
ctx,
buildedInline,
drawRect,
offset,
paddingTop,
paddingBottom,
col,
row,
grid
);
paddingTop += lineHeight;
paddingBottom -= lineHeight;
});
if (trailingIconInline) {
// Draw trailing icon
let maxWidth = 0;
buildedMultiInlines.forEach((buildedInline) => {
let sumWidth = 0;
buildedInline.forEach((inline) => {
sumWidth += inline.width({ ctx });
});
maxWidth = Math.max(maxWidth, sumWidth);
});
const baseRect = new Rect(
drawRect.left,
drawRect.top,
drawRect.width,
drawRect.height
);
const trailingIconRect = baseRect.copy();
if (width < maxWidth) {
trailingIconRect.left =
trailingIconRect.right - trailingIconWidth - offset;
} else {
trailingIconRect.left += maxWidth;
}
trailingIconRect.right = baseRect.right;
drawInlines(
ctx,
[trailingIconInline],
trailingIconRect,
offset,
0,
0,
col,
row,
grid
);
}
}
function calcElapsedColor(
startColor: string,
endColor: string,
elapsedTime: number
): string {
const startColorRGB = colorToRGB(startColor);
const endColorRGB = colorToRGB(endColor);
const getRGB = (colorName: keyof RGBA): number => {
const start = startColorRGB[colorName];
const end = endColorRGB[colorName];
if (elapsedTime >= 1) {
return end;
}
if (elapsedTime <= 0) {
return start;
}
const diff = start - end;
return Math.ceil(start - diff * elapsedTime);
};
return `rgb(${getRGB("r")}, ${getRGB("g")}, ${getRGB("b")})`;
}
function drawCheckbox<T>(
ctx: CanvasRenderingContext2D,
rect: RectProps,
col: number,
row: number,
check: boolean,
helper: GridCanvasHelper<T>,
{
animElapsedTime = 1,
uncheckBgColor = helper.theme.checkbox.uncheckBgColor,
checkBgColor = helper.theme.checkbox.checkBgColor,
borderColor = helper.theme.checkbox.borderColor,
textAlign = "center",
textBaseline = "middle",
}: {
animElapsedTime?: number;
uncheckBgColor?: ColorPropertyDefine;
checkBgColor?: ColorPropertyDefine;
borderColor?: ColorPropertyDefine;
textAlign?: CanvasTextAlign;
textBaseline?: CanvasTextBaseline;
},
positionOpt: {
offset?: number;
padding?: PaddingOption;
} = {}
): void {
const boxWidth = canvashelper.measureCheckbox(ctx).width;
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
const pos = calcStartPosition(
ctx,
rect,
boxWidth + 1 /*罫線分+1*/,
boxWidth + 1 /*罫線分+1*/,
positionOpt
);
uncheckBgColor = helper.getColor(uncheckBgColor, col, row, ctx);
checkBgColor = helper.getColor(checkBgColor, col, row, ctx);
borderColor = helper.getColor(borderColor, col, row, ctx);
if (0 < animElapsedTime && animElapsedTime < 1) {
uncheckBgColor = check
? uncheckBgColor
: calcElapsedColor(
checkBgColor as string,
uncheckBgColor as string,
animElapsedTime
);
checkBgColor = check
? calcElapsedColor(
uncheckBgColor as string,
checkBgColor as string,
animElapsedTime
)
: checkBgColor;
}
canvashelper.drawCheckbox(
ctx,
pos.x,
pos.y,
check ? animElapsedTime : false,
{
uncheckBgColor,
checkBgColor,
borderColor,
}
);
}
function drawRadioButton<T>(
ctx: CanvasRenderingContext2D,
rect: RectProps,
col: number,
row: number,
check: boolean,
helper: GridCanvasHelper<T>,
{
animElapsedTime = 1,
checkColor = helper.theme.radioButton.checkColor,
uncheckBorderColor = helper.theme.radioButton.uncheckBorderColor,
checkBorderColor = helper.theme.radioButton.checkBorderColor,
uncheckBgColor = helper.theme.radioButton.uncheckBgColor,
checkBgColor = helper.theme.radioButton.checkBgColor,
textAlign = "center",
textBaseline = "middle",
}: {
animElapsedTime?: number;
checkColor?: ColorPropertyDefine;
uncheckBorderColor?: ColorPropertyDefine;
checkBorderColor?: ColorPropertyDefine;
uncheckBgColor?: ColorPropertyDefine;
checkBgColor?: ColorPropertyDefine;
textAlign?: CanvasTextAlign;
textBaseline?: CanvasTextBaseline;
},
positionOpt = {}
): void {
const boxWidth = canvashelper.measureRadioButton(ctx).width;
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
const pos = calcStartPosition(
ctx,
rect,
boxWidth + 1 /*罫線分+1*/,
boxWidth + 1 /*罫線分+1*/,
positionOpt
);
checkColor = helper.getColor(checkColor, col, row, ctx);
uncheckBorderColor = helper.getColor(uncheckBorderColor, col, row, ctx);
checkBorderColor = helper.getColor(checkBorderColor, col, row, ctx);
uncheckBgColor = helper.getColor(uncheckBgColor, col, row, ctx);
checkBgColor = helper.getColor(checkBgColor, col, row, ctx);
let borderColor = check ? checkBorderColor : uncheckBorderColor;
let bgColor = check ? checkBgColor : uncheckBgColor;
if (0 < animElapsedTime && animElapsedTime < 1) {
borderColor = check
? calcElapsedColor(
uncheckBorderColor as string,
checkBorderColor as string,
animElapsedTime
)
: calcElapsedColor(
checkBorderColor as string,
uncheckBorderColor as string,
animElapsedTime
);
bgColor = check
? calcElapsedColor(
uncheckBgColor as string,
checkBgColor as string,
animElapsedTime
)
: calcElapsedColor(
checkBgColor as string,
uncheckBgColor as string,
animElapsedTime
);
}
canvashelper.drawRadioButton(
ctx,
pos.x,
pos.y,
check ? animElapsedTime : 1 - animElapsedTime,
{
checkColor,
borderColor,
bgColor,
}
);
}
class ThemeResolver<T> implements RequiredThemeDefine {
private _grid: ListGridAPI<T>;
private _checkbox: RequiredThemeDefine["checkbox"] | null = null;
private _radioButton: RequiredThemeDefine["radioButton"] | null = null;
private _button: RequiredThemeDefine["button"] | null = null;
private _tree: RequiredThemeDefine["tree"] | null = null;
private _header: RequiredThemeDefine["header"] | null = null;
private _messages: RequiredThemeDefine["messages"] | null = null;
private _indicators: RequiredThemeDefine["indicators"] | null = null;
constructor(grid: ListGridAPI<T>) {
this._grid = grid;
}
getThemeValue<
T extends ColorPropertyDefine | ColorsPropertyDefine | FontPropertyDefine
>(...name: string[]): T {
return getThemeValue(this._grid, ...name);
}
get font(): string {
return getThemeValue(this._grid, "font");
}
get underlayBackgroundColor(): string {
return getThemeValue(this._grid, "underlayBackgroundColor");
}
// color
get color(): ColorPropertyDefine {
return getThemeValue(this._grid, "color");
}
get frozenRowsColor(): ColorPropertyDefine {
return getThemeValue(this._grid, "frozenRowsColor");
}
// background
get defaultBgColor(): ColorPropertyDefine {
return getThemeValue(this._grid, "defaultBgColor");
}
get frozenRowsBgColor(): ColorPropertyDefine {
return getThemeValue(this._grid, "frozenRowsBgColor");
}
get selectionBgColor(): ColorPropertyDefine {
return getThemeValue(this._grid, "selectionBgColor");
}
get highlightBgColor(): ColorPropertyDefine {
return getThemeValue(this._grid, "highlightBgColor");
}
// border
get borderColor(): ColorsPropertyDefine {
return getThemeValue(this._grid, "borderColor");
}
get frozenRowsBorderColor(): ColorsPropertyDefine {
return getThemeValue(this._grid, "frozenRowsBorderColor");
}
get highlightBorderColor(): ColorsPropertyDefine {
return getThemeValue(this._grid, "highlightBorderColor");
}
get checkbox(): RequiredThemeDefine["checkbox"] {
const grid = this._grid;
return (
this._checkbox ||
(this._checkbox = {
get uncheckBgColor(): ColorPropertyDefine {
return getCheckboxProp("uncheckBgColor");
},
get checkBgColor(): ColorPropertyDefine {
return getCheckboxProp("checkBgColor");
},
get borderColor(): ColorPropertyDefine {
return getCheckboxProp("borderColor");
},
})
);
function getCheckboxProp(prop: string): ColorPropertyDefine {
return getThemeValue(grid, "checkbox", prop);
}
}
get radioButton(): RequiredThemeDefine["radioButton"] {
const grid = this._grid;
return (
this._radioButton ||
(this._radioButton = {
get checkColor(): ColorPropertyDefine {
return getRadioButtonProp("checkColor");
},
get uncheckBorderColor(): ColorPropertyDefine {
return getRadioButtonProp("uncheckBorderColor");
},
get checkBorderColor(): ColorPropertyDefine {
return getRadioButtonProp("checkBorderColor");
},
get uncheckBgColor(): ColorPropertyDefine {
return getRadioButtonProp("uncheckBgColor");
},
get checkBgColor(): ColorPropertyDefine {
return getRadioButtonProp("checkBgColor");
},
})
);
function getRadioButtonProp(prop: string): ColorPropertyDefine {
return getThemeValue(grid, "radioButton", prop);
}
}
get button(): RequiredThemeDefine["button"] {
const grid = this._grid;
return (
this._button ||
(this._button = {
get color(): ColorPropertyDefine {
return getButtonProp("color");
},
get bgColor(): ColorPropertyDefine {
return getButtonProp("bgColor");
},
})
);
function getButtonProp(prop: string): ColorPropertyDefine {
return getThemeValue(grid, "button", prop);
}
}
get tree(): RequiredThemeDefine["tree"] {
const grid = this._grid;
return (
this._tree ||
(this._tree = {
get lineStyle(): TreeLineStyle {
return getTreeProp("lineStyle");
},
get lineColor(): ColorPropertyDefine {
return getTreeProp("lineColor");
},
get lineWidth(): number {
return getTreeProp("lineWidth");
},
get treeIcon(): TreeBranchIconStyleDefine {
return getTreeProp("treeIcon");
},
})
);
function getTreeProp<
T extends
| ColorPropertyDefine
| number
| TreeLineStyle
| TreeBranchIconStyleDefine
>(prop: string): T {
return getThemeValue(grid, "tree", prop);
}
}
get header(): RequiredThemeDefine["header"] {
const grid = this._grid;
return (
this._header ||
(this._header = {
get sortArrowColor(): ColorPropertyDefine {
return getThemeValue(grid, "header", "sortArrowColor");
},
})
);
}
get messages(): RequiredThemeDefine["messages"] {
const grid = this._grid;
return (
this._messages ||
(this._messages = {
get infoBgColor(): ColorPropertyDefine {
return getMessageProp("infoBgColor");
},
get errorBgColor(): ColorPropertyDefine {
return getMessageProp("errorBgColor");
},
get warnBgColor(): ColorPropertyDefine {
return getMessageProp("warnBgColor");
},
get boxWidth(): number {
return getMessageProp("boxWidth");
},
get markHeight(): number {
return getMessageProp("markHeight");
},
})
);
function getMessageProp<T extends ColorPropertyDefine | number>(
prop: string
): T {
return getThemeValue(grid, "messages", prop);
}
}
get indicators(): RequiredThemeDefine["indicators"] {
const grid = this._grid;
return (
this._indicators ||
(this._indicators = {
get topLeftColor(): ColorPropertyDefine {
return getIndicatorsProp("topLeftColor");
},
get topLeftSize(): number {
return getIndicatorsProp("topLeftSize");
},
get topRightColor(): ColorPropertyDefine {
return getIndicatorsProp("topRightColor");
},
get topRightSize(): number {
return getIndicatorsProp("topRightSize");
},
get bottomRightColor(): ColorPropertyDefine {
return getIndicatorsProp("bottomRightColor");
},
get bottomRightSize(): number {
return getIndicatorsProp("bottomRightSize");
},
get bottomLeftColor(): ColorPropertyDefine {
return getIndicatorsProp("bottomLeftColor");
},
get bottomLeftSize(): number {
return getIndicatorsProp("bottomLeftSize");
},
})
);
function getIndicatorsProp<T extends ColorPropertyDefine | number>(
prop: string
): T {
return getThemeValue(grid, "indicators", prop);
}
}
}
function strokeRect(
ctx: CanvasRenderingContext2D,
color: ColorsDef,
left: number,
top: number,
width: number,
height: number
): void {
if (!Array.isArray(color)) {
if (color) {
ctx.strokeStyle = color;
ctx.strokeRect(left, top, width, height);
}
} else {
const borderColors = toBoxArray(color);
canvashelper.strokeColorsRect(ctx, borderColors, left, top, width, height);
}
}
function getPaddedRect(
rect: RectProps,
padding: number | string | (number | string)[] | undefined,
font: string | undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
helper: GridCanvasHelper<any>,
context: CellContext
) {
if (!padding) {
return rect;
}
const {
0: pTop,
1: pRight,
2: pBottom,
3: pLeft,
} = helper.toBoxPixelArray(padding, context, font);
const left = rect.left + pLeft;
const top = rect.top + pTop;
const width = rect.width - pRight - pLeft;
const height = rect.height - pTop - pBottom;
return new Rect(left, top, width, height);
}
export class GridCanvasHelper<T> implements GridCanvasHelperAPI {
private _grid: ListGridAPI<T>;
private _theme: RequiredThemeDefine;
constructor(grid: ListGridAPI<T>) {
this._grid = grid;
this._theme = new ThemeResolver(grid);
}
createCalculator(
context: CellContext,
font: string | undefined
): {
calcWidth(width: number | string): number;
calcHeight(height: number | string): number;
} {
return {
calcWidth(width: number | string): number {
return calc.toPx(width, {
get full() {
const rect = context.getRect();
return rect.width;
},
get em() {
return getFontSize(context.getContext(), font).width;
},
});
},
calcHeight(height: number | string): number {
return calc.toPx(height, {
get full() {
const rect = context.getRect();
return rect.height;
},
get em() {
return getFontSize(context.getContext(), font).height;
},
});
},
};
}
getColor(
color: ColorPropertyDefine,
col: number,
row: number,
ctx: CanvasRenderingContext2D
): ColorDef;
getColor(
color: ColorsPropertyDefine,
col: number,
row: number,
ctx: CanvasRenderingContext2D
): ColorsDef;
getColor(
color: ColorPropertyDefine | ColorsPropertyDefine,
col: number,
row: number,
ctx: CanvasRenderingContext2D
): ColorsDef {
return getStyleProperty(color, col, row, this._grid, ctx);
}
getStyleProperty<T>(
style: T | ((args: StylePropertyFunctionArg) => T),
col: number,
row: number,
ctx: CanvasRenderingContext2D
): T {
return getStyleProperty(style, col, row, this._grid, ctx);
}
toBoxArray(
obj: ColorsDef
): [ColorDef | null, ColorDef | null, ColorDef | null, ColorDef | null] {
return toBoxArray(obj);
}
toBoxPixelArray(
value: number | string | (number | string)[],
context: CellContext,
font: string | undefined
): [number, number, number, number] {
if (typeof value === "string" || Array.isArray(value)) {
const calculator = this.createCalculator(context, font);
const box = toBoxArray(value);
return [
calculator.calcHeight(box[0]),
calculator.calcWidth(box[1]),
calculator.calcHeight(box[2]),
calculator.calcWidth(box[3]),
];
}
return toBoxArray(value);
}
get theme(): RequiredThemeDefine {
return this._theme;
}
drawWithClip(
context: CellContext,
draw: (ctx: CanvasRenderingContext2D) => void
): void {
const drawRect = context.getDrawRect();
if (!drawRect) {
return;
}
const ctx = context.getContext();
ctx.save();
try {
ctx.beginPath();
ctx.rect(drawRect.left, drawRect.top, drawRect.width, drawRect.height);
//clip
ctx.clip();
draw(ctx);
} finally {
ctx.restore();
}
}
drawBorderWithClip(
context: CellContext,
draw: (ctx: CanvasRenderingContext2D) => void
): void {
const drawRect = context.getDrawRect();
if (!drawRect) {
return;
}
const rect = context.getRect();
const ctx = context.getContext();
ctx.save();
try {
//罫線用clip
ctx.beginPath();
let clipLeft = drawRect.left;
let clipWidth = drawRect.width;
if (drawRect.left === rect.left) {
clipLeft += -1;
clipWidth += 1;
}
let clipTop = drawRect.top;
let clipHeight = drawRect.height;
if (drawRect.top === rect.top) {
clipTop += -1;
clipHeight += 1;
}
ctx.rect(clipLeft, clipTop, clipWidth, clipHeight);
ctx.clip();
draw(ctx);
} finally {
ctx.restore();
}
}
text(
text: string | (Inline | string)[],
context: CellContext,
{
padding,
offset = TEXT_OFFSET,
color,
textAlign = "left",
textBaseline = "middle",
font,
textOverflow = "clip",
icons,
trailingIcon,
}: {
padding?: number | string | (number | string)[];
offset?: number;
color?: ColorPropertyDefine;
textAlign?: CanvasTextAlign;
textBaseline?: CanvasTextBaseline;
font?: FontPropertyDefine;
textOverflow?: TextOverflow;
icons?: SimpleColumnIconOption[];
trailingIcon?: SimpleColumnIconOption;
} = {}
): void {
const { col, row } = context;
if (!color) {
({ color } = this.theme);
// header color
const isFrozenCell = this._grid.isFrozenCell(col, row);
if (isFrozenCell && isFrozenCell.row) {
color = this.theme.frozenRowsColor;
}
}
this.drawWithClip(context, (ctx) => {
font = getFont(font, context.col, context.row, this._grid, ctx);
const rect = getPaddedRect(
context.getRect(),
padding,
font,
this,
context
);
_inlineRect(this._grid, ctx, text, rect, col, row, {
offset,
color,
textAlign,
textBaseline,
font,
textOverflow,
icons,
trailingIcon,
});
});
}
multilineText(
lines: string[],
context: CellContext,
{
padding,
offset = TEXT_OFFSET,
color,
textAlign = "left",
textBaseline = "middle",
font,
lineHeight = "1em",
autoWrapText = false,
lineClamp = 0,
textOverflow = "clip",
icons,
trailingIcon,
}: {
padding?: number | string | (number | string)[];
offset?: number;
color?: ColorPropertyDefine;
textAlign?: CanvasTextAlign;
textBaseline?: CanvasTextBaseline;
font?: FontPropertyDefine;
lineHeight?: string | number;
autoWrapText?: boolean;
lineClamp?: LineClamp;
textOverflow?: TextOverflow;
icons?: SimpleColumnIconOption[];
trailingIcon?: SimpleColumnIconOption;
} = {}
): void {
const { col, row } = context;
if (!color) {
({ color } = this.theme);
// header color
const isFrozenCell = this._grid.isFrozenCell(col, row);
if (isFrozenCell && isFrozenCell.row) {
color = this.theme.frozenRowsColor;
}
}
this.drawWithClip(context, (ctx) => {
font = getFont(font, context.col, context.row, this._grid, ctx);
const rect = getPaddedRect(
context.getRect(),
padding,
font,
this,
context
);
const calculator = this.createCalculator(context, font);
lineHeight = calculator.calcHeight(lineHeight);
_multiInlineRect(this._grid, ctx, lines, rect, col, row, {
offset,
color,
textAlign,
textBaseline,
font,
lineHeight,
autoWrapText,
lineClamp,
textOverflow,
icons,
trailingIcon,
});
});
}
fillText(
text: string,
x: number,
y: number,
context: CellContext,
{
color,
textAlign = "left",
textBaseline = "top",
font,
}: {
color?: ColorPropertyDefine;
textAlign?: CanvasTextAlign;
textBaseline?: CanvasTextBaseline;
font?: FontPropertyDefine;
} = {}
): void {
const { col, row } = context;
if (!color) {
({ color } = this.theme);
// header color
const isFrozenCell = this._grid.isFrozenCell(col, row);
if (isFrozenCell && isFrozenCell.row) {
color = this.theme.frozenRowsColor;
}
}
const ctx = context.getContext();
ctx.save();
try {
font = getFont(font, context.col, context.row, this._grid, ctx);
ctx.fillStyle = getStyleProperty(color, col, row, this._grid, ctx);
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = font || ctx.font;
ctx.fillText(text, x, y);
} finally {
ctx.restore();
}
}
fillCell(
context: CellContext,
{
fillColor = this.theme.defaultBgColor,
}: { fillColor?: ColorPropertyDefine } = {}
): void {
const rect = context.getRect();
this.drawWithClip(context, (ctx) => {
const { col, row } = context;
ctx.fillStyle = getStyleProperty(fillColor, col, row, this._grid, ctx);
ctx.beginPath();
ctx.rect(rect.left, rect.top, rect.width, rect.height);
ctx.fill();
});
}
fillCellWithState(
context: CellContext,
option: { fillColor?: ColorPropertyDefine } = {}
): void {
option.fillColor = this.getFillColorState(context, option);
this.fillCell(context, option);
}
fillRect(
rect: RectProps,
context: CellContext,
{
fillColor = this.theme.defaultBgColor,
}: { fillColor?: ColorPropertyDefine } = {}
): void {
const ctx = context.getContext();
ctx.save();
try {
const { col, row } = context;
ctx.fillStyle = getStyleProperty(fillColor, col, row, this._grid, ctx);
ctx.beginPath();
ctx.rect(rect.left, rect.top, rect.width, rect.height);
ctx.fill();
} finally {
ctx.restore();
}
}
fillRectWithState(
rect: RectProps,
context: CellContext,
option: { fillColor?: ColorPropertyDefine } = {}
): void {
option.fillColor = this.getFillColorState(context, option);
this.fillRect(rect, context, option);
}
getFillColorState(
context: CellContext,
option: { fillColor?: ColorPropertyDefine } = {}
): ColorPropertyDefine {
const sel = context.getSelection();
const { col, row } = context;
if (!cellEquals(sel.select, context) && cellInRange(sel.range, col, row)) {
return this.theme.selectionBgColor;
}
if (option.fillColor) {
return option.fillColor;
}
if (cellEquals(sel.select, context)) {
return this.theme.highlightBgColor;
}
const isFrozenCell = this._grid.isFrozenCell(col, row);
if (isFrozenCell && isFrozenCell.row) {
return this.theme.frozenRowsBgColor;
}
return this.theme.defaultBgColor;
}
border(
context: CellContext,
{
borderColor = this.theme.borderColor,
lineWidth = 1,
}: { borderColor?: ColorsPropertyDefine; lineWidth?: number } = {}
): void {
const rect = context.getRect();
this.drawBorderWithClip(context, (ctx) => {
const { col, row } = context;
const borderColors = getStyleProperty(
borderColor,
col,
row,
this._grid,
ctx
);
if (lineWidth === 1) {
ctx.lineWidth = 1;
strokeRect(
ctx,
borderColors,
rect.left - 0.5,
rect.top - 0.5,
rect.width,
rect.height
);
} else if (lineWidth === 2) {
ctx.lineWidth = 2;
strokeRect(
ctx,
borderColors,
rect.left,
rect.top,
rect.width - 1,
rect.height - 1
);
} else {
ctx.lineWidth = lineWidth;
const startOffset = lineWidth / 2 - 1;
strokeRect(
ctx,
borderColors,
rect.left + startOffset,
rect.top + startOffset,
rect.width - lineWidth + 1,
rect.height - lineWidth + 1
);
}
});
}
// Unused in main
borderWithState(
context: CellContext,
option: { borderColor?: ColorsPropertyDefine; lineWidth?: number } = {}
): void {
const rect = context.getRect();
const sel = context.getSelection();
const { col, row } = context;
//罫線
if (cellEquals(sel.select, context)) {
option.borderColor = this.theme.highlightBorderColor;
option.lineWidth = 2;
this.border(context, option);
} else {
// header color
const isFrozenCell = this._grid.isFrozenCell(col, row);
if (isFrozenCell?.row) {
option.borderColor = this.theme.frozenRowsBorderColor;
}
option.lineWidth = 1;
this.border(context, option);
//追加処理
const sel = this._grid.selection.select;
if (sel.col + 1 === col && sel.row === row) {
//右が選択されている
this.drawBorderWithClip(context, (ctx) => {
const borderColors = toBoxArray(
getStyleProperty(
this.theme.highlightBorderColor,
sel.col,
sel.row,
this._grid,
ctx
)
);
ctx.lineWidth = 1;
ctx.strokeStyle = borderColors[1] || ctx.strokeStyle;
ctx.beginPath();
ctx.moveTo(rect.left - 0.5, rect.top);
ctx.lineTo(rect.left - 0.5, rect.bottom);
ctx.stroke();
});
} else if (sel.col === col && sel.row + 1 === row) {
//上が選択されている
this.drawBorderWithClip(context, (ctx) => {
const borderColors = toBoxArray(
getStyleProperty(
this.theme.highlightBorderColor,
sel.col,
sel.row,
this._grid,
ctx
)
);
ctx.lineWidth = 1;
ctx.strokeStyle = borderColors[0] || ctx.strokeStyle;
ctx.beginPath();
ctx.moveTo(rect.left, rect.top - 0.5);
ctx.lineTo(rect.right, rect.top - 0.5);
ctx.stroke();
});
}
}
}
buildCheckBoxInline(
check: boolean,
context: CellContext,
option: Parameters<GridCanvasHelperAPI["buildCheckBoxInline"]>[2] = {}
): InlineDrawer {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const ctx = context.getContext();
const boxWidth = canvashelper.measureCheckbox(ctx).width;
return new InlineDrawer({
draw,
width: boxWidth + 3,
height: boxWidth + 1,
color: undefined,
});
function draw({
ctx,
rect,
offset,
offsetLeft,
offsetRight,
offsetTop,
offsetBottom,
}: InlineDrawOption): void {
const { col, row } = context;
drawCheckbox(ctx, rect, col, row, check, self, option, {
offset: offset + (CHECKBOX_OFFSET - TEXT_OFFSET),
padding: {
left: offsetLeft + (CHECKBOX_OFFSET - TEXT_OFFSET),
right: offsetRight,
top: offsetTop,
bottom: offsetBottom,
},
});
}
}
checkbox(
check: boolean,
context: CellContext,
{
padding,
animElapsedTime,
offset = CHECKBOX_OFFSET,
uncheckBgColor,
checkBgColor,
borderColor,
textAlign,
textBaseline,
}: Parameters<GridCanvasHelperAPI["checkbox"]>[2] = {}
): void {
this.drawWithClip(context, (ctx) => {
const { col, row } = context;
drawCheckbox(
ctx,
getPaddedRect(
context.getRect(),
padding,
undefined /* font */,
this,
context
),
col,
row,
check,
this,
{
animElapsedTime,
uncheckBgColor,
checkBgColor,
borderColor,
textAlign,
textBaseline,
},
{ offset, padding: { left: CHECKBOX_OFFSET - TEXT_OFFSET } }
);
});
}
radioButton(
check: boolean,
context: CellContext,
{
padding,
animElapsedTime,
offset = CHECKBOX_OFFSET,
checkColor,
uncheckBorderColor,
checkBorderColor,
uncheckBgColor,
checkBgColor,
textAlign,
textBaseline,
}: Parameters<GridCanvasHelperAPI["radioButton"]>[2] = {}
): void {
this.drawWithClip(context, (ctx) => {
const { col, row } = context;
drawRadioButton(
ctx,
getPaddedRect(
context.getRect(),
padding,
undefined /* font */,
this,
context
),
col,
row,
check,
this,
{
animElapsedTime,
checkColor,
uncheckBorderColor,
checkBorderColor,
uncheckBgColor,
checkBgColor,
textAlign,
textBaseline,
},
{ offset, padding: { left: CHECKBOX_OFFSET - TEXT_OFFSET } }
);
});
}
button(
caption: string,
context: CellContext,
{
bgColor = this.theme.button.bgColor,
padding,
offset = TEXT_OFFSET,
color = this.theme.button.color,
textAlign = "center",
textBaseline = "middle",
shadow,
font,
textOverflow = "clip",
icons,
}: Parameters<GridCanvasHelperAPI["button"]>[2] = {}
): void {
const rect = context.getRect();
this.drawWithClip(context, (ctx) => {
font = getFont(font, context.col, context.row, this._grid, ctx);
const { col, row } = context;
const { left, top, width, height } = getPaddedRect(
rect,
padding || rect.height / 8,
font,
this,
context
);
bgColor = getStyleProperty(
bgColor,
context.col,
context.row,
this._grid,
ctx
);
canvashelper.drawButton(ctx, left, top, width, height, {
bgColor,
radius: rect.height / 8,
// offset,
shadow,
});
_inlineRect(
this._grid,
ctx,
caption,
new Rect(left, top, width, height),
col,
row,
{
offset,
color,
textAlign,
textBaseline,
font,
textOverflow,
icons,
}
);
});
}
testFontLoad(
font: string | undefined,
value: string,
context: CellContext
): boolean {
return testFontLoad(font, value, context, this._grid);
}
}