Implementing Custom Cells
Coming soon. For now enjoy this custom cell which kind of explains the process.
import {
type CustomCell,
measureTextCached,
type CustomRenderer,
getMiddleCenterBias,
GridCellKind,
} from "@glideapps/glide-data-grid";
import * as React from "react";
import { roundedRect } from "../draw-fns.js";
interface RangeCellProps {
readonly kind: "range-cell";
readonly value: number;
readonly min: number;
readonly max: number;
readonly step: number;
readonly label?: string;
readonly measureLabel?: string;
readonly readonly?: boolean;
}
export type RangeCell = CustomCell<RangeCellProps>;
const RANGE_HEIGHT = 6;
const inputStyle: React.CSSProperties = {
marginRight: 8,
};
const wrapperStyle: React.CSSProperties = {
display: "flex",
alignItems: "center",
flexGrow: 1,
};
const renderer: CustomRenderer<RangeCell> = {
kind: GridCellKind.Custom,
isMatch: (c): c is RangeCell => (c.data as any).kind === "range-cell",
draw: (args, cell) => {
const { ctx, theme, rect } = args;
const { min, max, value, label, measureLabel } = cell.data;
const x = rect.x + theme.cellHorizontalPadding;
const yMid = rect.y + rect.height / 2;
const rangeSize = max - min;
const fillRatio = (value - min) / rangeSize;
ctx.save();
let labelWidth = 0;
if (label !== undefined) {
ctx.font = `12px ${theme.fontFamily}`; // fixme this is slow
labelWidth =
measureTextCached(measureLabel ?? label, ctx, `12px ${theme.fontFamily}`).width +
theme.cellHorizontalPadding;
}
const rangeWidth = rect.width - theme.cellHorizontalPadding * 2 - labelWidth;
if (rangeWidth >= RANGE_HEIGHT) {
const gradient = ctx.createLinearGradient(x, yMid, x + rangeWidth, yMid);
gradient.addColorStop(0, theme.accentColor);
gradient.addColorStop(fillRatio, theme.accentColor);
gradient.addColorStop(fillRatio, theme.bgBubble);
gradient.addColorStop(1, theme.bgBubble);
ctx.beginPath();
ctx.fillStyle = gradient;
roundedRect(ctx, x, yMid - RANGE_HEIGHT / 2, rangeWidth, RANGE_HEIGHT, RANGE_HEIGHT / 2);
ctx.fill();
ctx.beginPath();
roundedRect(
ctx,
x + 0.5,
yMid - RANGE_HEIGHT / 2 + 0.5,
rangeWidth - 1,
RANGE_HEIGHT - 1,
(RANGE_HEIGHT - 1) / 2
);
ctx.strokeStyle = theme.accentLight;
ctx.lineWidth = 1;
ctx.stroke();
}
if (label !== undefined) {
ctx.textAlign = "right";
ctx.fillStyle = theme.textDark;
ctx.fillText(
label,
rect.x + rect.width - theme.cellHorizontalPadding,
yMid + getMiddleCenterBias(ctx, `12px ${theme.fontFamily}`)
);
}
ctx.restore();
return true;
},
provideEditor: () => {
// eslint-disable-next-line react/display-name
return p => {
const { data } = p.value;
const strValue = data.value.toString();
const strMin = data.min.toString();
const strMax = data.max.toString();
const strStep = data.step.toString();
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
p.onChange({
...p.value,
data: {
...data,
value: Number(e.target.value),
},
});
};
return (
<label style={wrapperStyle}>
<input
style={inputStyle}
type="range"
value={strValue}
min={strMin}
max={strMax}
step={strStep}
onChange={onChange}
/>
{strValue}
</label>
);
};
},
onPaste: (v, d) => {
let num = Number.parseFloat(v);
num = Number.isNaN(num) ? d.value : Math.max(d.min, Math.min(d.max, num));
return {
...d,
value: num,
};
},
};
export default renderer;
Last updated