Skip to content

Cell Decorator API: composable rendering addons for standard cells #5

@witqq

Description

@witqq

Summary

A new API that allows extending the base cell renderer with composable "decorators" — custom rendering areas around or over the standard cell content, where the base text renderer automatically adjusts its available area.

Problem

Currently there are two extremes:

  1. Standard cell rendering — just text with theme styles, no extensibility
  2. Custom CellTypeRenderer.render() — completely replaces the text rendering pipeline, consumer must re-implement text layout, truncation, wrapping

There is nothing in between. For common patterns like "standard text cell + expander icon on the left" or "standard text cell + sort icon on the right", consumers must either:

  • Write a full custom renderer (duplicating text layout logic)
  • Create an overlay RenderLayer that paints over the engine output

Proposed API

interface CellDecorator {
  id: string;
  position: "left" | "right" | "overlay" | "underlay";
  
  // How much space to reserve (for left/right positions)
  getWidth(cellData: CellData, cellHeight: number, theme: SpreadsheetTheme): number;
  
  // Render the decorator in its allocated area
  render(ctx: CanvasRenderingContext2D, 
         x: number, y: number, width: number, height: number,
         cellData: CellData, theme: SpreadsheetTheme): void;
  
  // Optional: declare hit zones within the decorator area
  getHitZones?(width: number, height: number, cellData: CellData): CellHitZone[];
}

Usage example — expander decorator:

const expanderDecorator: CellDecorator = {
  id: "tree-expander",
  position: "left",
  getWidth(cellData, cellHeight) {
    const level = cellData.metadata?.hierarchyLevel ?? 0;
    const hasChildren = cellData.metadata?.hasChildren;
    return level * 20 + (hasChildren ? 16 : 0); // indent + icon
  },
  render(ctx, x, y, w, h, cellData, theme) {
    // Draw indent + expander triangle
  },
  getHitZones(w, h, cellData) {
    return [{ id: "toggle", x: w - 16, y: (h - 12) / 2, width: 12, height: 12 }];
  }
};

engine.getCellTypeRegistry().addDecorator("tree-expander", expanderDecorator, {
  appliesTo: (row, col, cellData) => cellData.metadata?.hierarchyLevel != null
});

How it works in the rendering pipeline:

  1. CellTextLayer collects applicable decorators for each cell
  2. Left decorators: reserve width from the left → text area starts after them
  3. Right decorators: reserve width from the right → text truncation width reduced
  4. Underlay decorators: render before text (e.g., progress bar background)
  5. Overlay decorators: render after text (e.g., status badges)
  6. Text layout uses the remaining area after decorator space is reserved

Benefits:

  • Base text rendering (font, color, alignment, truncation, wrapping) stays intact
  • Decorators compose — multiple left/right decorators stack
  • Hit zones integrate with the sub-cell hit testing system (issue Sub-cell hit testing API: clickable zones within cells #4)
  • Covers expanders, sort icons, action buttons, progress bars, status indicators

Use Cases

  • Tree/hierarchy expanders (left decorator with indent)
  • Sort indicators in data cells (right decorator)
  • Action buttons (right decorator with hit zone)
  • Progress bar backgrounds (underlay decorator)
  • Validation/status badges (overlay decorator)
  • Images before/after text (left/right decorator)

Related

Files to modify

  • packages/core/src/types/cell-type-registry.tsCellDecorator interface, registration
  • packages/core/src/renderer/layers/cell-text-layer.ts — decorator space calculation, rendering
  • packages/core/src/types/interfaces.ts — new types

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions