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

Renderer API #2005

Closed
2 of 5 tasks
Tyriar opened this issue Apr 8, 2019 · 3 comments
Closed
2 of 5 tasks

Renderer API #2005

Tyriar opened this issue Apr 8, 2019 · 3 comments
Assignees
Labels
area/api type/proposal A proposal that needs some discussion before proceeding

Comments

@Tyriar
Copy link
Member

Tyriar commented Apr 8, 2019

An API for renderers is necessary if we want to include the WebGL renderer as an optional component. A lot of this issue is about refactoring the renderer code to share a lot of the core functionality of all the renderers to make the actual IRenderer API interface slimmer for addons to eventually implement. Here are the sub-goals:

  • Move ColorManager to live under Terminal, pass IColorSet to the renderer
  • Implement the renderer "orchestrator" that does the debouncing/pausing etc. that the current renderers do.
  • Look into whether CharMeasure can live only in the DOM renderer code (canvas renderers can use onOptionsChange)
  • Break renderer dependence on Terminal by passing in relevant objects to handlers Renderer addons will have access to the xterm.js API
  • Expose the renderer API as experimental

API proposal

Note that the inline proposal will change as progress is made.

class Terminal {
  /**
   * Gets or sets the renderer to use for the `Terminal`, the renderer is
   * responsible for displaying all terminal data to the user, including the
   * mouse selection.
   */ 
  renderer: IRenderer;
}

/**
 * An interface that implements a renderer capable of rendering terminal data.
 * Note that if the browser supports `IntersectionObserver` it will not call to
 * render frames when the terminal is completely hidden. The best way to
 * implement this interface is to keep a 
 */
interface IRenderer extends IDisposable {
  /**
   * The dimensions that the renderer will use for things like canvas, cell and
   * character size.
   */
  readonly dimensions: IRenderDimensions;

  /**
   * A method that is called when `window.devicePixelRatio` changes.
   */
  onDevicePixelRatioChange(): void;

  /**
   * A method that is called when the dimenisons of the termnial change.
   */
  onResize(cols: number, rows: number): void;

  /**
   * A method that is called when the terminal loses focus.
   */
  onBlur(): void;

  /**
   * A method that is called when the terminal gains focus.
   */
  onFocus(): void;

  /**
   * A method that is called when the terminal cursor moves, this may be useful
   * for managing the state of a blinking cursor.
   */
  onCursorMove(): void;
  
  /**
   * A method that is called when terminal options change. Note that changes to
   * the theme should be handled with `IRenderer.onThemeChange`.
   */
  onOptionsChange(): void;

  /**
   * A method that is called when the theme changes.
   */
  onThemeChange(colors: IColorSet): void;
  
  /**
   * Clears the viewport.
   */
  clear(): void;

  /**
   * Renders the terminal selection. Depending on the renderer, this method
   * should either render the selection or generate a model to be used when
   * `renderRows` is called, for the latter you must return `true` to guarentee
   * `renderRows` will be called even if no rows have changed.
   * @param start The start of the selection in `[x, y]` format.
   * @param end The end of the selection in `[x, y]` format.
   * @param columnSelectMode Whether column select mode is on, if so the
   * selection should be rendered as a square with start being the top-left
   * corner and end being the bottom-right corner.
   * @returns Whether `renderRows` should be triggered (do this if
   * `renderSelection` only generates a model to render in `renderRows`.
   */
  onSelectionChange(start: [number, number], end: [number, number], columnSelectMode: boolean): boolean;
  
  // TODO: Should buffer be a stripped down view of the world? An iterator? It needs to remain performant.

  /**
   * Renders a range of rows. Calls made to this function are debounced such
   * that they will only be called during an animation frame.
   * @param buffer The terminal buffer on which to render.
   * @param start The first row to render (valid values: `0` to
   * `Terminal.rows - 1`).
   * @param end The last row to render (valid values: `0` to
   * `Terminal.rows - 1`).
   */
  renderRows(buffer: IBuffer, start: number, end: number): void;

  registerCharacterJoiner(handler: CharacterJoinerHandler): number;
  deregisterCharacterJoiner(joinerId: number): boolean;
}

export interface IRenderDimensions {
  canvasWidth: number;
  canvasHeight: number;
  cellWidth: number;
  cellHeight: number;
  scaledCharWidth: number;
  scaledCharHeight: number;
  scaledCellWidth: number;
  scaledCellHeight: number;
  scaledCharLeft: number;
  scaledCharTop: number;
  scaledCanvasWidth: number;
  scaledCanvasHeight: number;
}
@Tyriar Tyriar added type/proposal A proposal that needs some discussion before proceeding area/renderer area/api labels Apr 8, 2019
@Tyriar Tyriar self-assigned this Apr 8, 2019
@mofux
Copy link
Contributor

mofux commented Apr 8, 2019

IMHO we should add another abstraction between the renderer and the buffer in this step. We are still passing the raw terminal buffer to the renderer, which has the following disadvantages:

  • Renderer and Buffer have to live in the same thread
  • The renderer has to know and operate on the internals of the Buffer
  • It provides no abstraction for rendering other things, like highlighted search text or link decorations

I think we need some kind of a view model in the middle that communicates with the buffer and provides a more abstract representation of it to the renderer. This view model would also be a good place for plugins to provide cell decorations (e.g. search highlight, link underline).

Having a view model would also de-duplicate alot of the logic currently done by every renderer:

  • figure out which cells to skip and which to draw double-wide
  • figure out text color
  • figure out background color
  • figure out the boldAsBright stuff

The model might produce an array of per-cell paint instructions to the renderer, only including the cells that need a redraw:

interface ICellPaint {
  
  /** The 0-based x-index of the cell inside the viewport */
  x: number;

  /** The 0-based y-index of the cell inside the viewport */
  y: number;

  /** The character to draw in the cell */
  char: string | void;

  /** 
   * The width of the cell, can be either 1 or 2 (for doube-width chars). 
   * Note: We won't generate ICellPaint for 0-width cells 
   */
  width: number;

  /** Cell styles like Italic, Bold or Underline */
  styles: ICellPaintStyle[];

  /** The foreground color of the text */
  fg: IColor;

  /** The background color of the cell */
  bg: IColor;

  /** A reference to the decorations targeting this cell */
  decorations: IDecoration[];

}

The interface on IRenderer might then look like this:

interface IRenderer extends IDisposable {
  // ...
  renderCells(paints: ICellPaint[]): void
  // ...
}

Now, scrolling the viewport up or down would lead to a full redraw with every line scrolled, which is not ideal. We can be smarter by introducing different types of instructions the renderer should execute. I'm currently thinking of two types of instructions: One instruction that causes the renderer to pull or push the viewport up or down by n rows, cutting off the rows that fall out of the viewport, and another one that provides ICellPaint[] instructions.

interface IRenderer extends IDisposable {
  // ...
  // forget about renderCells, render does replace it
  // ...
  render(instructions: IPaintInstruction[]): void
  // ...
}

As an example, if we scroll the viewport up one row, the instructions generated might look like this:

// the IPaintInstruction[] passed to the renderer
[
  // positive offset pushes rows down, negative pull them up
  { type: RenderInstructionType.Shift, offset: 1 },

  // only contains cell instructions for row 0, as the others don't need a redraw
  { type: RenderInstructionType.PaintCell, paints: [ICellPaint, ICellPaint, ...] }
]

From an implementation point of view, it might look like this (DOM renderer):

render(instructions: IPaintInstruction[]) {

  // iterate instructions
  for (let instruction of instructions) {
    
    // render instruction
    switch (instruction.type) {

      case RendererInstructionType.Shift: {
        // based on the offset, either remove dom rows at the top and add empty ones
        // at the bottom, or the other way around
      }

      case RendererInstructionType.PaintCell: {
        for (let paint of instruction.paints) {
          // create or update cell spans, set character, apply styles and so on...
          const cellSpan = this.cellDomNodes[paint.y][paint.x];
          cellSpan.textContent = paint.char;
        }
      }

    }

  }

}

I think it would also make sense to have an instruction to resize the viewport, and possibly decorations could also get their own instruction type later on.

Is this too complex?

@Tyriar
Copy link
Member Author

Tyriar commented Apr 8, 2019

@mofux 👍 yeah something like that sounds good and should resolve my TODO about the buffer, I think we could reuse the cells and only create new objects after a resize. I think we should stick to rendering rows and not get into paint instructions though (yet?), that really complicates things. Will revise tonight 🙂

@Tyriar
Copy link
Member Author

Tyriar commented Jun 16, 2019

Decided against exposing this so we can move the webgl renderer forward #1790 (comment), decorations are still on the table to allow addons to customize what's shown.

@Tyriar Tyriar closed this as completed Jun 16, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/api type/proposal A proposal that needs some discussion before proceeding
Projects
None yet
Development

No branches or pull requests

2 participants