diff --git a/packages/engine-render/src/components/sheets/extensions/row-header-layout.ts b/packages/engine-render/src/components/sheets/extensions/row-header-layout.ts index c2b613b606..1aeb40d466 100644 --- a/packages/engine-render/src/components/sheets/extensions/row-header-layout.ts +++ b/packages/engine-render/src/components/sheets/extensions/row-header-layout.ts @@ -21,18 +21,83 @@ 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; + 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: `${rowIndex + 1}` }; + } + 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) { + 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; } @@ -50,19 +115,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++) { @@ -71,21 +134,66 @@ 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.stroke(); diff --git a/packages/engine-render/src/components/sheets/interfaces.ts b/packages/engine-render/src/components/sheets/interfaces.ts index 6ea895b40c..7ac4800de9 100644 --- a/packages/engine-render/src/components/sheets/interfaces.ts +++ b/packages/engine-render/src/components/sheets/interfaces.ts @@ -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 | {}; } @@ -108,7 +107,6 @@ export interface IPaintForScrolling { scaleX: number; scaleY: number; } - export interface IColumnStyleCfg { fontFamily: string; fontColor: string; @@ -121,3 +119,16 @@ export interface IColumnStyleCfg { export type IAColumnCfgObj = IColumnStyleCfg & { text: string }; export type IAColumnCfg = undefined | null | string | Partial; + +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; diff --git a/packages/engine-render/src/components/sheets/row-header.ts b/packages/engine-render/src/components/sheets/row-header.ts index 5e4cfa4d02..54dd93004d 100644 --- a/packages/engine-render/src/components/sheets/row-header.ts +++ b/packages/engine-render/src/components/sheets/row-header.ts @@ -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'; @@ -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); + } } diff --git a/packages/facade/src/apis/__tests__/facade.spec.ts b/packages/facade/src/apis/__tests__/facade.spec.ts index 9616e74e58..c19e5d5374 100644 --- a/packages/facade/src/apis/__tests__/facade.spec.ts +++ b/packages/facade/src/apis/__tests__/facade.spec.ts @@ -20,8 +20,15 @@ import { ICommandService, IUniverInstanceService } from '@univerjs/core'; import { SetRangeValuesCommand, SetRangeValuesMutation, SetStyleCommand } from '@univerjs/sheets'; import type { Injector } from '@wendellhu/redi'; -import type { ColumnHeaderLayout, RenderComponentType, SheetComponent, SpreadsheetColumnHeader } 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'; @@ -294,6 +301,7 @@ describe('Test FUniver', () => { 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); @@ -303,4 +311,23 @@ describe('Test FUniver', () => { 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(); + }); }); diff --git a/packages/facade/src/apis/facade.ts b/packages/facade/src/apis/facade.ts index fbd3d2d103..93af956d6a 100644 --- a/packages/facade/src/apis/facade.ts +++ b/packages/facade/src/apis/facade.ts @@ -34,8 +34,15 @@ import { IRegisterFunctionService, RegisterFunctionService } from '@univerjs/she import type { Dependency, IDisposable } from '@wendellhu/redi'; import { Inject, Injector, Quantity } from '@wendellhu/redi'; -import type { IColumnsHeaderCfgParam, RenderComponentType, SheetComponent, SheetExtension, SpreadsheetColumnHeader } from '@univerjs/engine-render'; -import { IRenderManagerService } from '@univerjs/engine-render'; +import type { + IColumnsHeaderCfgParam, + IRowsHeaderCfgParam, + RenderComponentType, + SheetComponent, + SheetExtension, SpreadsheetColumnHeader, + SpreadsheetRowHeader } from '@univerjs/engine-render'; +import { + IRenderManagerService } from '@univerjs/engine-render'; import { SHEET_VIEW_KEY } from '@univerjs/sheets-ui'; import { SetFormulaCalculationStartMutation } from '@univerjs/engine-formula'; import { FDocument } from './docs/f-document'; @@ -360,7 +367,7 @@ export class FUniver { * customizeColumnHeader * @param cfg * cfg example - ({ headerStyle:{backgroundColor: 'pink', fontSize: 9}, columnsCfg: ['MokaII', undefined, null, {text: 'Size', textAlign: 'left'}]}) + ({ headerStyle:{backgroundColor: 'pink', fontSize: 9}, columnsCfg: ['MokaII', undefined, null, {text: 'Size', textAlign: 'left'}]}) */ customizeColumnHeader(cfg: IColumnsHeaderCfgParam) { const wb = this.getActiveWorkbook(); @@ -373,6 +380,17 @@ export class FUniver { sheetColumn.setCustomHeader(cfg); } + customizeRowHeader(cfg: IRowsHeaderCfgParam) { + const wb = this.getActiveWorkbook(); + if (!wb) { + console.error('WorkBook not exist'); + return; + } + const unitId = wb?.getId(); + const sheetRow = this._getSheetRenderComponent(unitId, SHEET_VIEW_KEY.ROW) as SpreadsheetRowHeader; + sheetRow.setCustomHeader(cfg); + } + private _initialize(): void { this._debouncedFormulaCalculation = debounce(() => { this._commandService.executeCommand(