Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: customize row header #2457

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Next Next commit
feat: customize row header
  • Loading branch information
Евгений Крютченко committed Jun 7, 2024
commit 1fec60a30e65a46add435af7371a6076076c625e
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,90 @@
*/

import type { IScale } from '@univerjs/core';
import { numberToABC } from '@univerjs/core';

import { DEFAULT_FONTFACE_PLANE, FIX_ONE_PIXEL_BLUR_OFFSET, MIDDLE_CELL_POS_MAGIC_NUMBER } from '../../../basics/const';
import { getColor } from '../../../basics/tools';
import type { UniverRenderingContext } from '../../../context';
import { SheetRowHeaderExtensionRegistry } from '../../extension';
import type { SpreadsheetSkeleton } from '../sheet-skeleton';
import type { IARowCfg, IARowCfgObj, IColumnStyleCfg, IRowStyleCfg } from '../interfaces.ts';
import { SheetExtension } from './sheet-extension';

const UNIQUE_KEY = 'DefaultRowHeaderLayoutExtension';

export interface IRowsHeaderCfgParam {
headerStyle?: Partial<IRowStyleCfg>;
rowsCfg?: IARowCfg[];
}

const DEFAULT_ROW_STYLE = {
fontSize: 13,
fontFamily: DEFAULT_FONTFACE_PLANE,
fontColor: '#000000',
backgroundColor: getColor([248, 249, 250]),
borderColor: getColor([217, 217, 217]),
textAlign: 'center',
textBaseline: 'middle',
} as const;

export class RowHeaderLayout extends SheetExtension {
override uKey = UNIQUE_KEY;

override Z_INDEX = 10;
rowsCfg: IARowCfg[] = [];
headerStyle: IRowStyleCfg = {
fontSize: DEFAULT_ROW_STYLE.fontSize,
fontFamily: DEFAULT_ROW_STYLE.fontFamily,
fontColor: DEFAULT_ROW_STYLE.fontColor,
backgroundColor: DEFAULT_ROW_STYLE.backgroundColor,
borderColor: DEFAULT_ROW_STYLE.borderColor,
textAlign: DEFAULT_ROW_STYLE.textAlign,
textBaseline: DEFAULT_ROW_STYLE.textBaseline,
};

constructor(cfg?: IRowsHeaderCfgParam) {
super();
if (cfg) {
this.configHeaderRow(cfg);
}
}

configHeaderRow(cfg: IRowsHeaderCfgParam) {
this.rowsCfg = cfg.rowsCfg || [];
this.headerStyle = { ...this.headerStyle, ...cfg.headerStyle };
}

getCfgOfCurrentRow(rowIndex: number) {
let mergeWithSpecCfg;
let curRowSpecCfg;
const rowsCfg = this.rowsCfg || [];

if (rowsCfg[rowIndex]) {
if (typeof rowsCfg[rowIndex] == 'string') {
rowsCfg[rowIndex] = { text: rowsCfg[rowIndex] } as IARowCfgObj;
}
curRowSpecCfg = rowsCfg[rowIndex] as IRowStyleCfg & { text: string };
mergeWithSpecCfg = { ...this.headerStyle, ...curRowSpecCfg };
} else {
mergeWithSpecCfg = { ...this.headerStyle, text: numberToABC(rowIndex) };
KrutchenkoEA marked this conversation as resolved.
Show resolved Hide resolved
}
const specStyle = Object.keys(curRowSpecCfg || {}).length > 1; // if cfg have more keys than 'text', means there would be special style config for this row.
return [mergeWithSpecCfg, specStyle] as [IARowCfgObj, boolean];
}

setStyleToCtx(ctx: UniverRenderingContext, rowStyle: Partial<IColumnStyleCfg>) {
if (rowStyle.textAlign) ctx.textAlign = rowStyle.textAlign;
if (rowStyle.textBaseline) ctx.textBaseline = rowStyle.textBaseline;
if (rowStyle.fontColor) ctx.fillStyle = rowStyle.fontColor;
if (rowStyle.borderColor) ctx.strokeStyle = rowStyle.borderColor;
if (rowStyle.fontSize) ctx.font = `${rowStyle.fontSize}px ${DEFAULT_FONTFACE_PLANE}`;
}

// eslint-disable-next-line max-lines-per-function
override draw(ctx: UniverRenderingContext, parentScale: IScale, spreadsheetSkeleton: SpreadsheetSkeleton) {
const { rowColumnSegment, rowHeaderWidth = 0 } = spreadsheetSkeleton;
const { startRow, endRow } = rowColumnSegment;

if (!spreadsheetSkeleton || rowHeaderWidth === 0) {
return;
}
Expand All @@ -50,19 +116,17 @@ export class RowHeaderLayout extends SheetExtension {
}

const scale = this._getScale(parentScale);
this.setStyleToCtx(ctx, this.headerStyle);

ctx.fillStyle = getColor([248, 249, 250])!;
// background
ctx.save();
ctx.fillStyle = this.headerStyle.backgroundColor;
ctx.fillRectByPrecision(0, 0, rowHeaderWidth, rowTotalHeight);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = getColor([0, 0, 0])!;
ctx.beginPath();
ctx.setLineWidthByPrecision(1);
ctx.restore();

ctx.setLineWidthByPrecision(1);
ctx.translateWithPrecisionRatio(FIX_ONE_PIXEL_BLUR_OFFSET, FIX_ONE_PIXEL_BLUR_OFFSET);

ctx.strokeStyle = getColor([217, 217, 217])!;
ctx.font = `13px ${DEFAULT_FONTFACE_PLANE}`;
let preRowPosition = 0;
const rowHeightAccumulationLength = rowHeightAccumulation.length;
for (let r = startRow - 1; r <= endRow; r++) {
Expand All @@ -71,23 +135,68 @@ export class RowHeaderLayout extends SheetExtension {
}
const rowEndPosition = rowHeightAccumulation[r];
if (preRowPosition === rowEndPosition) {
// Skip hidden rows
continue;
continue; // Skip hidden rows
}
const cellBound = {
left: 0,
top: preRowPosition,
right: rowHeaderWidth,
bottom: rowEndPosition,
width: rowHeaderWidth,
height: rowEndPosition - preRowPosition,
};
const [curRowCfg, specStyle] = this.getCfgOfCurrentRow(r);

// background
if (specStyle && curRowCfg.backgroundColor) {
ctx.save();
ctx.fillStyle = curRowCfg.backgroundColor;
ctx.fillRectByPrecision(cellBound.left, cellBound.top, cellBound.width, cellBound.height);
ctx.restore();
}

// horizontal line border
ctx.beginPath();
ctx.moveToByPrecision(cellBound.left, cellBound.bottom);
ctx.lineToByPrecision(cellBound.right, cellBound.bottom);
ctx.stroke();

// row header text
const textX = (() => {
switch (curRowCfg.textAlign) {
case 'center':
return cellBound.left + (cellBound.right - cellBound.left) / 2;
case 'right':
return cellBound.right - MIDDLE_CELL_POS_MAGIC_NUMBER;
case 'left':
return cellBound.left + MIDDLE_CELL_POS_MAGIC_NUMBER;
default: // center
return cellBound.left + (cellBound.right - cellBound.left) / 2;
}
})();
const middleYCellRect = preRowPosition + (rowEndPosition - preRowPosition) / 2 + MIDDLE_CELL_POS_MAGIC_NUMBER; // Magic number 1, because the vertical alignment appears to be off by 1 pixel

if (specStyle) {
ctx.save();
ctx.beginPath();
this.setStyleToCtx(ctx, curRowCfg);
ctx.rectByPrecision(cellBound.left, cellBound.top, cellBound.width, cellBound.height);
ctx.clip();
}

ctx.fillText(curRowCfg.text, textX, middleYCellRect);
if (specStyle) {
ctx.restore();
}
ctx.moveToByPrecision(0, rowEndPosition);
ctx.lineToByPrecision(rowHeaderWidth, rowEndPosition);

const middleCellPos = preRowPosition + (rowEndPosition - preRowPosition) / 2;
ctx.fillText(`${r + 1}`, rowHeaderWidth / 2, middleCellPos + MIDDLE_CELL_POS_MAGIC_NUMBER); // Magic number 1, because the vertical alignment appears to be off by 1 pixel.
preRowPosition = rowEndPosition;
}
// console.log('xx2', rowColumnIndexRange, bounds, this._rowTotalHeight, this._rowHeightAccumulation);

// painting line bottom border
// border right line
const rowHeaderWidthFix = rowHeaderWidth - 0.5 / scale;

ctx.beginPath();
ctx.moveToByPrecision(rowHeaderWidthFix, 0);
ctx.lineToByPrecision(rowHeaderWidthFix, rowTotalHeight);
ctx.lineToByPrecision(0, rowTotalHeight);
ctx.stroke();
}
}
Expand Down
27 changes: 25 additions & 2 deletions packages/engine-render/src/components/sheets/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ import type { DocumentSkeleton } from '../docs/layout/doc-skeleton';
import type { Canvas } from '../../canvas';
import type { UniverRenderingContext } from '../../context';

export
interface BorderCache {
export interface BorderCache {
[key: string]: BorderCacheItem | {};
}

Expand Down Expand Up @@ -108,4 +107,28 @@ export interface IPaintForScrolling {
scaleX: number;
scaleY: number;
}
export interface IColumnStyleCfg {
fontFamily: string;
fontColor: string;
fontSize: number;
borderColor: string;
textAlign: CanvasTextAlign;
textBaseline: CanvasTextBaseline;
backgroundColor: string;
}

export type IAColumnCfgObj = IColumnStyleCfg & { text: string };
export type IAColumnCfg = undefined | null | string | Partial<IAColumnCfgObj>;

export interface IRowStyleCfg {
fontFamily: string;
fontColor: string;
fontSize: number;
borderColor: string;
textAlign: CanvasTextAlign;
textBaseline: CanvasTextBaseline;
backgroundColor: string;
}

export type IARowCfgObj = IColumnStyleCfg & { text: string };
export type IARowCfg = undefined | null | string | Partial<IARowCfgObj>;
9 changes: 7 additions & 2 deletions packages/engine-render/src/components/sheets/row-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { Nullable } from '@univerjs/core';
import type { IViewportInfo, Vector2 } from '../../basics/vector2';
import type { UniverRenderingContext } from '../../context';
import { SheetRowHeaderExtensionRegistry } from '../extension';
import type { RowHeaderLayout } from './extensions/row-header-layout';
import type { IRowsHeaderCfgParam, RowHeaderLayout } from './extensions/row-header-layout';
import { SpreadsheetHeader } from './sheet-component';
import type { SpreadsheetSkeleton } from './sheet-skeleton';

Expand Down Expand Up @@ -79,7 +79,7 @@ export class SpreadsheetRowHeader extends SpreadsheetHeader {
}

override isHit(coord: Vector2) {
const oCoord = this.getInverseCoord(coord);
const oCoord = this._getInverseCoord(coord);
const skeleton = this.getSkeleton();
if (!skeleton) {
return false;
Expand All @@ -97,4 +97,9 @@ export class SpreadsheetRowHeader extends SpreadsheetHeader {
});
this._rowHeaderLayoutExtension = this.getExtensionByKey('DefaultRowHeaderLayoutExtension') as RowHeaderLayout;
}

setCustomHeader(cfg: IRowsHeaderCfgParam) {
this.makeDirty(true);
this._rowHeaderLayoutExtension.configHeaderRow(cfg);
}
}
69 changes: 58 additions & 11 deletions packages/facade/src/apis/__tests__/facade.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@
* limitations under the License.
*/

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ICellData, IStyleData, Nullable } from '@univerjs/core';
import { ICommandService, IUniverInstanceService } from '@univerjs/core';
import { SetRangeValuesCommand, SetRangeValuesMutation, SetStyleCommand } from '@univerjs/sheets';
import type { Injector } from '@wendellhu/redi';

import type { RenderComponentType, SheetComponent } from '@univerjs/engine-render';
import { IRenderManagerService } from '@univerjs/engine-render';
import type {
ColumnHeaderLayout,
RenderComponentType,
RowHeaderLayout,
SheetComponent,
SpreadsheetColumnHeader,
SpreadsheetRowHeader } from '@univerjs/engine-render';
import {
IRenderManagerService } from '@univerjs/engine-render';
import { SHEET_VIEW_KEY } from '@univerjs/sheets-ui';
import { RegisterFunctionMutation, SetFormulaCalculationStartMutation, UnregisterFunctionMutation } from '@univerjs/engine-formula';
import { IDescriptionService } from '@univerjs/sheets-formula';
Expand Down Expand Up @@ -102,6 +109,16 @@ describe('Test FUniver', () => {

return renderComponent;
};

vi.useFakeTimers();
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
return setTimeout(callback, 16);
});
});

afterEach(() => {
(window.requestAnimationFrame as any).mockRestore();
vi.useRealTimers();
});

it('Function onBeforeCommandExecute', () => {
Expand Down Expand Up @@ -261,18 +278,48 @@ describe('Test FUniver', () => {
it('Function getSnapshot', () => {
const snapshot = univerAPI.getActiveWorkbook()?.getSnapshot();
expect(snapshot?.resources).toEqual([
{
data: '{}',
name: 'SHEET_WORKSHEET_PROTECTION_PLUGIN',
},
{
data: '{}',
name: 'SHEET_WORKSHEET_PROTECTION_POINT_PLUGIN',
},
{
name: 'SHEET_CONDITIONAL_FORMATTING_PLUGIN',
data: '{"sheet-0011":[{"cfId":"AEGZdW8C","ranges":[{"startRow":2,"startColumn":1,"endRow":11,"endColumn":5,"startAbsoluteRefType":0,"endAbsoluteRefType":0,"rangeType":0}],"rule":{"type":"highlightCell","subType":"text","operator":"containsText","style":{"cl":{"rgb":"#2f56ef"},"bg":{"rgb":"#e8ecfc"}},"value":""},"stopIfTrue":false},{"cfId":"4ICEXdJj","ranges":[{"startRow":2,"startColumn":1,"endRow":11,"endColumn":5,"startAbsoluteRefType":0,"endAbsoluteRefType":0,"rangeType":0}],"rule":{"type":"highlightCell","subType":"text","operator":"containsText","style":{"cl":{"rgb":"#2f56ef"},"bg":{"rgb":"#e8ecfc"}},"value":""},"stopIfTrue":false},{"cfId":"geCv018z","ranges":[{"startRow":2,"startColumn":1,"endRow":11,"endColumn":5,"startAbsoluteRefType":0,"endAbsoluteRefType":0,"rangeType":0}],"rule":{"type":"highlightCell","subType":"text","operator":"containsText","style":{"cl":{"rgb":"#2f56ef"},"bg":{"rgb":"#e8ecfc"}},"value":""},"stopIfTrue":false}]}',
},
]);
});

it('Function customizeColumnHeader', () => {
const unitId = univerAPI.getActiveWorkbook()?.getId() || '';
const columnRenderComp = getSheetRenderComponent(unitId, SHEET_VIEW_KEY.COLUMN) as SpreadsheetColumnHeader;
if (!columnRenderComp) return;
const columnHeaderExt = columnRenderComp.extensions.get('DefaultColumnHeaderLayoutExtension')! as ColumnHeaderLayout;

const spy = vi.spyOn(columnHeaderExt, 'draw');

univerAPI.customizeColumnHeader({ headerStyle: { backgroundColor: 'pink', fontSize: 9 }, columnsCfg: ['ASC', 'MokaII', undefined, { text: 'Size', textAlign: 'left' }, { text: 'MUJI', fontSize: 15, textAlign: 'right' }, { text: 'SRI-RESOLVE', fontSize: 10, textAlign: 'left', fontColor: 'blue', backgroundColor: 'wheat' }, null, null, 'ss', { fontSize: 29, fontColor: 'red', text: 'hash' }] });
univerAPI.customizeColumnHeader({ headerStyle: { backgroundColor: 'pink', fontSize: 9 }, columnsCfg: ['ASC', 'MokaII', undefined, { text: 'Size', textAlign: 'left' }, { text: 'MUJI', fontSize: 15, textAlign: 'right' }, { text: 'SRI-RESOLVE', fontSize: 10, textAlign: 'left', fontColor: 'blue', backgroundColor: 'wheat' }, null, null, 'ss', { fontSize: 29, fontColor: 'red', text: 'hash' }] });
expect(columnHeaderExt.headerStyle.backgroundColor).toBe('pink');
expect(columnHeaderExt.headerStyle.fontSize).toBe(9);
expect(columnHeaderExt.headerStyle.borderColor).toBe('rgb(217,217,217)');
expect(columnHeaderExt.columnsCfg.length).toBe(10);

vi.advanceTimersByTime(16); // mock time pass by
expect(spy).toHaveBeenCalled();
});

it('Function customizeRowHeader', () => {
const unitId = univerAPI.getActiveWorkbook()?.getId() || '';
const rowRenderComp = getSheetRenderComponent(unitId, SHEET_VIEW_KEY.ROW) as SpreadsheetRowHeader;
if (!rowRenderComp) return;
const rowHeaderExt = rowRenderComp.extensions.get('DefaultRowHeaderLayoutExtension')! as RowHeaderLayout;

const spy = vi.spyOn(rowHeaderExt, 'draw');

univerAPI.customizeRowHeader({ headerStyle: { backgroundColor: 'pink', fontSize: 9 }, rowsCfg: ['ASC', 'MokaII', undefined, { text: 'Size', textAlign: 'left' }, { text: 'MUJI', fontSize: 15, textAlign: 'right' }, { text: 'SRI-RESOLVE', fontSize: 10, textAlign: 'left', fontColor: 'blue', backgroundColor: 'wheat' }, null, null, 'ss', { fontSize: 29, fontColor: 'red', text: 'hash' }] });
univerAPI.customizeRowHeader({ headerStyle: { backgroundColor: 'pink', fontSize: 9 }, rowsCfg: ['ASC', 'MokaII', undefined, { text: 'Size', textAlign: 'left' }, { text: 'MUJI', fontSize: 15, textAlign: 'right' }, { text: 'SRI-RESOLVE', fontSize: 10, textAlign: 'left', fontColor: 'blue', backgroundColor: 'wheat' }, null, null, 'ss', { fontSize: 29, fontColor: 'red', text: 'hash' }] });
expect(rowHeaderExt.headerStyle.backgroundColor).toBe('pink');
expect(rowHeaderExt.headerStyle.fontSize).toBe(9);
expect(rowHeaderExt.headerStyle.borderColor).toBe('rgb(217,217,217)');
expect(rowHeaderExt.rowsCfg.length).toBe(10);

vi.advanceTimersByTime(16); // mock time pass by
expect(spy).toHaveBeenCalled();
});
});