Загрузка данных


import { IChartApi, IPrimitivePaneView, PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
import { Observable } from 'rxjs';

import {
  CustomPriceAxisPaneView,
  CustomPriceAxisView,
  CustomTimeAxisPaneView,
  CustomTimeAxisView,
} from '@core/Drawings/axis';
import { SeriesDrawingBase } from '@core/Drawings/common';
import {
  getAnchorFromPoint,
  getPriceDelta as getPriceDeltaFromCoordinates,
  getXCoordinateFromTime,
  getYCoordinateFromPrice,
  isNearPoint,
  shiftTimeByPixels,
} from '@core/Drawings/helpers';
import { updateViews } from '@core/Drawings/utils';

import { getThemeStore } from '@src/theme';
import { Defaults } from '@src/types/defaults';
import { formatPrice } from '@src/utils';
import { formatDate } from '@src/utils/formatter';

import { TrendLinePaneView } from './paneView';

import {
  createDefaultSettings,
  getTrendLineSettingsTabs,
  TrendLineSettings,
  TrendLineStyle,
  TrendLineTextStyle,
} from './settings';

import type { ISeriesDrawing } from '@core/Drawings/common';
import type { Anchor, AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';
import type { ChartOptionsModel, SettingsTab } from '@src/types';

type TrendLineMode = 'idle' | 'drawing' | 'ready' | 'dragging-start' | 'dragging-end' | 'dragging-body';
type TimeLabelKind = 'start' | 'end';
type PriceLabelKind = 'start' | 'end';

interface TrendLineParams {
  container: HTMLElement;
  formatObservable?: Observable<ChartOptionsModel>;
  removeSelf?: () => void;
  openSettings?: () => void;
}

interface TrendLineState {
  hidden: boolean;
  isActive: boolean;
  mode: TrendLineMode;
  startAnchor: Anchor | null;
  endAnchor: Anchor | null;
  settings: TrendLineSettings;
}

interface TrendLineGeometry {
  startPoint: Point;
  endPoint: Point;
  left: number;
  right: number;
  top: number;
  bottom: number;
}

export interface TrendLineRenderData extends TrendLineGeometry, TrendLineStyle, TrendLineTextStyle {
  showHandles: boolean;
}

const LINE_HIT_TOLERANCE = 6;
const MIN_LINE_SIZE = 4;

export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements ISeriesDrawing {
  private removeSelf?: () => void;
  private openSettings?: () => void;

  protected settings: TrendLineSettings = createDefaultSettings();

  protected mode: TrendLineMode = 'idle';

  private startAnchor: Anchor | null = null;
  private endAnchor: Anchor | null = null;

  private dragPointerId: number | null = null;
  private dragStartPoint: Point | null = null;
  private dragStateSnapshot: TrendLineState | null = null;

  private displayFormat: ChartOptionsModel = {
    dateFormat: Defaults.dateFormat,
    timeFormat: Defaults.timeFormat,
    showTime: Defaults.showTime,
  };

  private readonly paneView: TrendLinePaneView;
  private readonly timeAxisPaneView: CustomTimeAxisPaneView;
  private readonly priceAxisPaneView: CustomPriceAxisPaneView;
  private readonly startTimeAxisView: CustomTimeAxisView;
  private readonly endTimeAxisView: CustomTimeAxisView;
  private readonly startPriceAxisView: CustomPriceAxisView;
  private readonly endPriceAxisView: CustomPriceAxisView;

  constructor(
    chart: IChartApi,
    series: SeriesApi,
    { container, formatObservable, removeSelf, openSettings }: TrendLineParams,
  ) {
    super({ chart, series, container });
    this.removeSelf = removeSelf;
    this.openSettings = openSettings;

    this.paneView = new TrendLinePaneView(this);

    this.timeAxisPaneView = new CustomTimeAxisPaneView({
      getAxisSegments: () => this.getTimeAxisSegments(),
    });

    this.priceAxisPaneView = new CustomPriceAxisPaneView({
      getAxisSegments: () => this.getPriceAxisSegments(),
    });

    this.startTimeAxisView = new CustomTimeAxisView({
      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
      labelKind: 'start',
    });

    this.endTimeAxisView = new CustomTimeAxisView({
      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
      labelKind: 'end',
    });

    this.startPriceAxisView = new CustomPriceAxisView({
      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
      labelKind: 'start',
    });

    this.endPriceAxisView = new CustomPriceAxisView({
      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
      labelKind: 'end',
    });

    if (formatObservable) {
      this.subscriptions.add(
        formatObservable.subscribe((format) => {
          this.displayFormat = format;
          this.render();
        }),
      );
    }

    this.series.attachPrimitive(this);
  }

  public isCreationPending(): boolean {
    return this.mode === 'idle' || this.mode === 'drawing';
  }

  public getState(): TrendLineState {
    return {
      hidden: this.hidden,
      isActive: this.isActive.value,
      mode: this.mode,
      startAnchor: this.startAnchor,
      endAnchor: this.endAnchor,
      settings: { ...this.settings },
    };
  }

  public setState(state: unknown): void {
    const nextState = state as Partial<TrendLineState>;

    if ('hidden' in nextState && typeof nextState.hidden === 'boolean') {
      this.hidden = nextState.hidden;
    }

    if ('isActive' in nextState && typeof nextState.isActive === 'boolean') {
      this.isActive.next(nextState.isActive);
    }

    if ('mode' in nextState && nextState.mode) {
      this.mode = nextState.mode;
    }

    if ('startAnchor' in nextState) {
      this.startAnchor = nextState.startAnchor ?? null;
    }

    if ('endAnchor' in nextState) {
      this.endAnchor = nextState.endAnchor ?? null;
    }

    if ('settings' in nextState && nextState.settings) {
      this.settings = {
        ...createDefaultSettings(),
        ...nextState.settings,
      };
    }

    this.render();
  }

  public getSettingsTabs(): SettingsTab[] {
    return getTrendLineSettingsTabs(this.settings);
  }

  public updateAllViews(): void {
    updateViews([
      this.paneView,
      this.timeAxisPaneView,
      this.priceAxisPaneView,
      this.startTimeAxisView,
      this.endTimeAxisView,
      this.startPriceAxisView,
      this.endPriceAxisView,
    ]);
  }

  public paneViews(): readonly IPrimitivePaneView[] {
    return [this.paneView];
  }

  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
    return [this.timeAxisPaneView];
  }

  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
    return [this.priceAxisPaneView];
  }

  public timeAxisViews() {
    return [this.startTimeAxisView, this.endTimeAxisView];
  }

  public priceAxisViews() {
    return [this.startPriceAxisView, this.endPriceAxisView];
  }

  public getRenderData(): TrendLineRenderData | null {
    if (this.hidden) {
      return null;
    }

    const geometry = this.getGeometry();

    if (!geometry) {
      return null;
    }

    return {
      ...geometry,
      showHandles: this.isActive.value,
      ...this.settings,
    };
  }

  public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
    if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
      return null;
    }

    const point = { x, y };

    if (this.getPointTarget(point)) {
      return {
        cursorStyle: 'move',
        externalId: 'trend-line',
        zOrder: 'top',
      };
    }

    if (!this.isPointNearLine(point)) {
      return null;
    }

    return {
      cursorStyle: 'grab',
      externalId: 'trend-line',
      zOrder: 'top',
    };
  }

  protected getTimeAxisSegments(): AxisSegment[] {
    if (!this.isActive.value) {
      return [];
    }

    const geometry = this.getGeometry();

    if (!geometry) {
      return [];
    }

    const { colors } = getThemeStore();

    return [
      {
        from: geometry.left,
        to: geometry.right,
        color: colors.axisMarkerAreaFill,
      },
    ];
  }

  protected getPriceAxisSegments(): AxisSegment[] {
    if (!this.isActive.value) {
      return [];
    }

    const geometry = this.getGeometry();

    if (!geometry) {
      return [];
    }

    const { colors } = getThemeStore();

    return [
      {
        from: geometry.top,
        to: geometry.bottom,
        color: colors.axisMarkerAreaFill,
      },
    ];
  }

  protected getTimeAxisLabel(kind: string): AxisLabel | null {
    if (!this.isActive.value || (kind !== 'start' && kind !== 'end')) {
      return null;
    }

    const coordinate = this.getTimeCoordinate(kind);
    const text = this.getTimeText(kind);

    if (coordinate === null || !text) {
      return null;
    }

    const { colors } = getThemeStore();

    return {
      coordinate,
      text,
      textColor: colors.chartPriceLineText,
      backgroundColor: colors.axisMarkerLabelFill,
    };
  }

  protected getPriceAxisLabel(kind: string): AxisLabel | null {
    if (!this.isActive.value || (kind !== 'start' && kind !== 'end')) {
      return null;
    }

    const coordinate = this.getPriceCoordinate(kind);
    const text = this.getPriceText(kind);

    if (coordinate === null || !text) {
      return null;
    }

    const { colors } = getThemeStore();

    return {
      coordinate,
      text,
      textColor: colors.chartPriceLineText,
      backgroundColor: colors.axisMarkerLabelFill,
    };
  }

  protected handleDoubleClick = (event: MouseEvent): void => {
    if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
      return;
    }

    const rect = this.container.getBoundingClientRect();
    const point = {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    };

    if (!this.getPointTarget(point) && !this.isPointNearLine(point)) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();

    this.openSettings?.();
  };

  protected handlePointerDown = (event: PointerEvent): void => {
    if (this.hidden || event.button !== 0) {
      return;
    }

    const point = this.getEventPoint(event);

    if (this.mode === 'idle') {
      event.preventDefault();
      event.stopPropagation();

      this.startDrawing(point);
      return;
    }

    if (this.mode === 'drawing') {
      event.preventDefault();
      event.stopPropagation();

      this.updateDrawing(point);
      this.finishDrawing();
      return;
    }

    if (this.mode !== 'ready') {
      return;
    }

    const pointTarget = this.getPointTarget(point);

    if (!this.isActive.value) {
      if (!pointTarget && !this.isPointNearLine(point)) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();

      this.isActive.next(true);
      this.render();
      return;
    }

    if (pointTarget === 'start') {
      event.preventDefault();
      event.stopPropagation();

      this.startDragging('dragging-start', point, event.pointerId);
      return;
    }

    if (pointTarget === 'end') {
      event.preventDefault();
      event.stopPropagation();

      this.startDragging('dragging-end', point, event.pointerId);
      return;
    }

    if (this.isPointNearLine(point)) {
      event.preventDefault();
      event.stopPropagation();

      this.startDragging('dragging-body', point, event.pointerId);
      return;
    }

    this.isActive.next(false);
    this.render();
  };

  protected handlePointerMove = (event: PointerEvent): void => {
    const point = this.getEventPoint(event);

    if (this.mode === 'drawing') {
      this.updateDrawing(point);
      return;
    }

    if (this.dragPointerId !== event.pointerId) {
      return;
    }

    if (this.mode === 'dragging-start' || this.mode === 'dragging-end') {
      event.preventDefault();
      event.stopPropagation();

      this.movePoint(point);
      this.render();
      return;
    }

    if (this.mode === 'dragging-body') {
      event.preventDefault();
      event.stopPropagation();

      this.moveBody(point);
      this.render();
    }
  };

  protected handlePointerUp = (event: PointerEvent): void => {
    if (this.dragPointerId !== event.pointerId) {
      return;
    }

    if (this.mode === 'dragging-start' || this.mode === 'dragging-end' || this.mode === 'dragging-body') {
      this.finishDragging();
    }
  };

  private startDrawing(point: Point): void {
    const anchor = this.createAnchor(point);

    if (!anchor) {
      return;
    }

    this.startAnchor = anchor;
    this.endAnchor = anchor;
    this.isActive.next(true);
    this.mode = 'drawing';

    this.render();
  }

  private updateDrawing(point: Point): void {
    const anchor = this.createAnchor(point);

    if (!anchor) {
      return;
    }

    this.endAnchor = anchor;
    this.render();
  }

  private finishDrawing(): void {
    const geometry = this.getGeometry();

    if (!geometry) {
      return;
    }

    const lineSize = Math.hypot(
      geometry.endPoint.x - geometry.startPoint.x,
      geometry.endPoint.y - geometry.startPoint.y,
    );

    if (lineSize < MIN_LINE_SIZE) {
      this.removeSelf?.();
      return;
    }

    this.mode = 'ready';
    this.resolveReady?.();

    this.render();
  }

  private startDragging(mode: TrendLineMode, point: Point, pointerId: number): void {
    this.mode = mode;

    this.dragPointerId = pointerId;
    this.dragStartPoint = point;
    this.dragStateSnapshot = this.getState();

    this.hideCrosshair();
    this.render();
  }

  private finishDragging(): void {
    this.mode = 'ready';
    this.resolveReady?.();

    this.dragPointerId = null;
    this.dragStartPoint = null;
    this.dragStateSnapshot = null;

    this.showCrosshair();
    this.render();
  }

  private movePoint(point: Point): void {
    const anchor = this.createAnchor(point);

    if (!anchor) {
      return;
    }

    if (this.mode === 'dragging-start') {
      this.startAnchor = anchor;
    }

    if (this.mode === 'dragging-end') {
      this.endAnchor = anchor;
    }
  }

  private moveBody(point: Point): void {
    const snapshot = this.dragStateSnapshot;

    if (!snapshot?.startAnchor || !snapshot.endAnchor || !this.dragStartPoint) {
      return;
    }

    const offsetX = point.x - this.dragStartPoint.x;
    const priceOffset = getPriceDeltaFromCoordinates(this.series, this.dragStartPoint.y, point.y);

    const nextStartTime = shiftTimeByPixels(this.chart, snapshot.startAnchor.time, offsetX, this.series);
    const nextEndTime = shiftTimeByPixels(this.chart, snapshot.endAnchor.time, offsetX, this.series);

    if (nextStartTime === null || nextEndTime === null) {
      return;
    }

    this.startAnchor = {
      time: nextStartTime,
      price: snapshot.startAnchor.price + priceOffset,
    };

    this.endAnchor = {
      time: nextEndTime,
      price: snapshot.endAnchor.price + priceOffset,
    };
  }

  private createAnchor(point: Point): Anchor | null {
    return getAnchorFromPoint(this.chart, this.series, point);
  }

  protected getGeometry(): TrendLineGeometry | null {
    if (!this.startAnchor || !this.endAnchor) {
      return null;
    }

    const startX = getXCoordinateFromTime(this.chart, this.startAnchor.time, this.series);
    const endX = getXCoordinateFromTime(this.chart, this.endAnchor.time, this.series);
    const startY = getYCoordinateFromPrice(this.series, this.startAnchor.price);
    const endY = getYCoordinateFromPrice(this.series, this.endAnchor.price);

    if (startX === null || endX === null || startY === null || endY === null) {
      return null;
    }

    const startPoint = {
      x: Math.round(Number(startX)),
      y: Math.round(Number(startY)),
    };

    const endPoint = {
      x: Math.round(Number(endX)),
      y: Math.round(Number(endY)),
    };

    return {
      startPoint,
      endPoint,
      left: Math.min(startPoint.x, endPoint.x),
      right: Math.max(startPoint.x, endPoint.x),
      top: Math.min(startPoint.y, endPoint.y),
      bottom: Math.max(startPoint.y, endPoint.y),
    };
  }

  private getPointTarget(point: Point): 'start' | 'end' | null {
    const geometry = this.getGeometry();

    if (!geometry) {
      return null;
    }

    if (isNearPoint(point, geometry.startPoint.x, geometry.startPoint.y, 8)) {
      return 'start';
    }

    if (isNearPoint(point, geometry.endPoint.x, geometry.endPoint.y, 8)) {
      return 'end';
    }

    return null;
  }

  private isPointNearLine(point: Point): boolean {
    const geometry = this.getGeometry();

    if (!geometry) {
      return false;
    }

    return this.getDistanceToSegment(point, geometry.startPoint, geometry.endPoint) <= LINE_HIT_TOLERANCE;
  }

  private getDistanceToSegment(point: Point, startPoint: Point, endPoint: Point): number {
    const dx = endPoint.x - startPoint.x;
    const dy = endPoint.y - startPoint.y;

    if (dx === 0 && dy === 0) {
      return Math.hypot(point.x - startPoint.x, point.y - startPoint.y);
    }

    const t = Math.max(
      0,
      Math.min(1, ((point.x - startPoint.x) * dx + (point.y - startPoint.y) * dy) / (dx * dx + dy * dy)),
    );

    const projectionX = startPoint.x + t * dx;
    const projectionY = startPoint.y + t * dy;

    return Math.hypot(point.x - projectionX, point.y - projectionY);
  }

  private getTimeCoordinate(kind: TimeLabelKind): number | null {
    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;

    if (!anchor) {
      return null;
    }

    const coordinate = getXCoordinateFromTime(this.chart, anchor.time, this.series);

    return coordinate === null ? null : Number(coordinate);
  }

  private getPriceCoordinate(kind: PriceLabelKind): number | null {
    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;

    if (!anchor) {
      return null;
    }

    const coordinate = getYCoordinateFromPrice(this.series, anchor.price);

    return coordinate === null ? null : Number(coordinate);
  }

  private getTimeText(kind: TimeLabelKind): string {
    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;

    if (!anchor || typeof anchor.time !== 'number') {
      return '';
    }

    return formatDate(
      anchor.time as UTCTimestamp,
      this.displayFormat.dateFormat,
      this.displayFormat.timeFormat,
      this.displayFormat.showTime,
    );
  }

  private getPriceText(kind: PriceLabelKind): string {
    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;

    if (!anchor) {
      return '';
    }

    return formatPrice(anchor.price) ?? '';
  }
}



import { getThemeStore } from '@src/theme';
import { t } from '@src/translations';

import { SettingField, SettingsTab, SettingsValues } from '@src/types';

export interface TrendLineStyle {
  lineColor: string;
}

export interface TrendLineTextStyle {
  fontSize: number;
  text: string;
  isBold: boolean;
  isItalic: boolean;
  textColor: string;
}

export type TrendLineSettings = TrendLineStyle & TrendLineTextStyle & SettingsValues;

export function createDefaultSettings(): TrendLineSettings {
  const { colors } = getThemeStore();

  return {
    lineColor: colors.chartLineColor,
    fontSize: 14,
    text: '',
    isBold: false,
    isItalic: false,
    textColor: colors.chartLineColor,
  };
}

export function getTrendLineSettingsTabs(settings: TrendLineSettings): SettingsTab[] {
  const styleFields: SettingField[] = [
    {
      key: 'lineColor',
      label: t('Line color'),
      type: 'color',
      defaultValue: settings.lineColor,
    },
  ];

  const textFields: SettingField[] = [
    {
      key: 'fontSize',
      label: t('Font size'),
      type: 'number',
      defaultValue: settings.fontSize,
      min: 8,
      max: 24,
    },
    {
      key: 'text',
      label: t('Text'),
      type: 'textarea',
      defaultValue: settings.text,
      placeholder: t('Enter text'),
    },
    {
      key: 'isBold',
      label: t('Bold'),
      type: 'boolean',
      defaultValue: settings.isBold,
    },
    {
      key: 'isItalic',
      label: t('Italics'),
      type: 'boolean',
      defaultValue: settings.isItalic,
    },
    {
      key: 'textColor',
      label: t('Text color'),
      type: 'color',
      defaultValue: settings.textColor,
    },
  ];

  return [
    {
      key: 'style',
      label: t('Style'),
      fields: styleFields,
    },
    {
      key: 'text',
      label: t('Text'),
      fields: textFields,
    },
  ];
}



import { IPrimitivePaneRenderer, IPrimitivePaneView, PrimitivePaneViewZOrder } from 'lightweight-charts';

import { TrendLinePaneRenderer } from './paneRenderer';
import { TrendLine } from './trendLine';

export class TrendLinePaneView implements IPrimitivePaneView {
  private readonly trendLine: TrendLine;

  constructor(trendLine: TrendLine) {
    this.trendLine = trendLine;
  }

  public update(): void {}

  public renderer(): IPrimitivePaneRenderer {
    return new TrendLinePaneRenderer(this.trendLine);
  }

  public zOrder(): PrimitivePaneViewZOrder {
    return 'top';
  }
}


import { CanvasRenderingTarget2D } from 'fancy-canvas';
import { IPrimitivePaneRenderer } from 'lightweight-charts';

import { getThemeStore } from '@src/theme';

import { TrendLine } from './trendLine';

const UI = {
  lineWidth: 2,
  handleRadius: 5,
  handleBorderWidth: 2,
  textLineHeightMultiplier: 1.2,
  textOffset: 4,
};

export class TrendLinePaneRenderer implements IPrimitivePaneRenderer {
  private readonly trendLine: TrendLine;

  constructor(trendLine: TrendLine) {
    this.trendLine = trendLine;
  }

  public draw(target: CanvasRenderingTarget2D): void {
    const data = this.trendLine.getRenderData();

    if (!data) {
      return;
    }

    const { colors } = getThemeStore();

    target.useBitmapCoordinateSpace(({ context, horizontalPixelRatio, verticalPixelRatio }) => {
      const pixelRatio = Math.max(horizontalPixelRatio, verticalPixelRatio);

      const startX = data.startPoint.x * horizontalPixelRatio;
      const startY = data.startPoint.y * verticalPixelRatio;
      const endX = data.endPoint.x * horizontalPixelRatio;
      const endY = data.endPoint.y * verticalPixelRatio;

      context.save();

      context.lineWidth = UI.lineWidth * pixelRatio;
      context.strokeStyle = data.lineColor;
      context.beginPath();
      context.moveTo(startX, startY);
      context.lineTo(endX, endY);
      context.stroke();

      if (data.text.trim()) {
        drawTextAlongLine(context, {
          startX,
          startY,
          endX,
          endY,
          text: data.text,
          fontSize: data.fontSize,
          isBold: data.isBold,
          isItalic: data.isItalic,
          textColor: data.textColor,
          horizontalPixelRatio,
          verticalPixelRatio,
        });
      }

      if (data.showHandles) {
        context.fillStyle = colors.chartBackground;
        context.strokeStyle = colors.chartLineColor;
        context.lineWidth = UI.handleBorderWidth * pixelRatio;

        drawHandle(context, startX, startY, UI.handleRadius * pixelRatio);
        drawHandle(context, endX, endY, UI.handleRadius * pixelRatio);
      }

      context.restore();
    });
  }
}

function drawHandle(context: CanvasRenderingContext2D, x: number, y: number, radius: number): void {
  context.beginPath();
  context.arc(x, y, radius, 0, Math.PI * 2);
  context.fill();
  context.stroke();
}

function drawTextAlongLine(
  context: CanvasRenderingContext2D,
  params: {
    startX: number;
    startY: number;
    endX: number;
    endY: number;
    text: string;
    fontSize: number;
    isBold: boolean;
    isItalic: boolean;
    textColor: string;
    horizontalPixelRatio: number;
    verticalPixelRatio: number;
  },
): void {
  const { startX, startY, endX, endY, text, fontSize, isBold, isItalic, textColor, verticalPixelRatio } = params;

  const lines = text.split('\n');
  const safeFontSize = Math.max(1, fontSize);
  const fontSizePx = safeFontSize * verticalPixelRatio;
  const lineHeight = safeFontSize * UI.textLineHeightMultiplier * verticalPixelRatio;

  const dx = endX - startX;
  const dy = endY - startY;

  let angle = Math.atan2(dy, dx);

  if (angle > Math.PI / 2 || angle < -Math.PI / 2) {
    angle += Math.PI;
  }

  const centerX = (startX + endX) / 2;
  const centerY = (startY + endY) / 2;

  const fontWeight = isBold ? '700 ' : '';
  const fontStyle = isItalic ? 'italic ' : '';

  context.save();
  context.translate(centerX, centerY);
  context.rotate(angle);

  context.font = `${fontStyle}${fontWeight}${fontSizePx}px Inter, sans-serif`;
  context.fillStyle = textColor;
  context.textAlign = 'center';
  context.textBaseline = 'middle';

  const blockHeight = lines.length * lineHeight;
  const textOffset = UI.textOffset * verticalPixelRatio;
  const textCenterY = -(blockHeight / 2 + textOffset);
  const startLineY = textCenterY - blockHeight / 2 + lineHeight / 2;

  lines.forEach((line, index) => {
    context.fillText(line, 0, startLineY + index * lineHeight);
  });

  context.restore();
}


import { TrendLinePaneRenderer } from './paneRenderer';
import { TrendLinePaneView } from './paneView';
import { TrendLine, TrendLineRenderData } from './trendLine';

export { TrendLine, TrendLinePaneRenderer, TrendLinePaneView };
export type { TrendLineRenderData };


import {
  AutoscaleInfo,
  CrosshairMode,
  IChartApi,
  IPrimitivePaneView,
  ISeriesApi,
  ISeriesPrimitive,
  ISeriesPrimitiveAxisView,
  Logical,
  PrimitiveHoveredItem,
  SeriesAttachedParameter,
  SeriesOptionsMap,
  SeriesType,
  Time,
} from 'lightweight-charts';

import { BehaviorSubject, distinctUntilChanged, Observable, Subscription } from 'rxjs';

import { getPointerPoint as getPointerPointFromEvent } from '@core/Drawings/helpers';
import { AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';

import { ChartOptionsModel, SettingsTab, SettingsValues } from '@src/types';

export interface ISeriesDrawing extends ISeriesPrimitive<Time> {
  show(): void;
  hide(): void;
  rebind(series: ISeriesApi<SeriesType>): void;
  destroy(): void;
  isCreationPending(): boolean;
  waitTillReady(): Promise<void>;

  shouldShowInObjectTree(): boolean;

  getState(): unknown;
  setState(state: unknown): void;

  getSettings(): SettingsValues;
  updateSettings(settings: SettingsValues): void;
  getSettingsTabs(): SettingsTab[];

  subscribeIsSelected(cb: (isSelected: boolean) => void): void;

  getRenderData(): unknown;
}

interface SeriesDrawingBaseParams {
  container: HTMLElement;
  chart: IChartApi;
  series: SeriesApi;
}

export abstract class SeriesDrawingBase<TSettings extends SettingsValues = SettingsValues> implements ISeriesDrawing {
  protected hidden = false;
  protected chart: IChartApi;
  protected series: SeriesApi;
  protected subscriptions = new Subscription();
  protected abstract mode: unknown; // todo: хочется иметь единый mode
  protected abstract settings: TSettings;
  protected readonly container: HTMLElement;
  protected isActive: BehaviorSubject<boolean> = new BehaviorSubject(false);
  protected isBound = false;

  protected readyPromise: null | Promise<void> = null;
  protected resolveReady: null | (() => void) = null;

  protected requestUpdate: (() => void) | null = null;

  constructor({ chart, series, container }: SeriesDrawingBaseParams) {
    this.chart = chart;
    this.series = series;
    this.container = container;
  }

  public subscribeIsSelected(cb: (isSelected: boolean) => void) {
    return this.isActive.pipe(distinctUntilChanged()).subscribe(cb);
  }

  public show(): void {
    this.hidden = false;
    this.render();
  }

  public hide(): void {
    this.hidden = true;
    this.showCrosshair();
    this.render();
  }

  public rebind(series: SeriesApi): void {
    if (this.series === series) {
      return;
    }

    this.showCrosshair();
    this.unbindEvents();
    this.series.detachPrimitive(this as unknown as ISeriesPrimitive<Time>);

    this.series = series;
    this.requestUpdate = null;

    this.series.attachPrimitive(this as unknown as ISeriesPrimitive<Time>);
    this.render();
  }

  public destroy(): void {
    this.showCrosshair();
    this.unbindEvents();
    this.subscriptions.unsubscribe();
    this.series.detachPrimitive(this as unknown as ISeriesPrimitive<Time>);
    this.requestUpdate = null;
    this.resolveReady?.();
  }

  public waitTillReady(): Promise<void> {
    if (this.mode === 'ready') {
      return Promise.resolve();
    }
    if (!this.readyPromise) {
      this.readyPromise = new Promise((resolve) => {
        this.resolveReady = resolve as () => void;
      });
    }

    return this.readyPromise;
  }

  public shouldShowInObjectTree(): boolean {
    return this.mode !== 'idle';
  }

  public getSettings(): SettingsValues {
    return { ...this.settings };
  }

  public updateSettings(settings: SettingsValues): void {
    this.settings = {
      ...this.settings,
      ...settings,
    };

    this.render();
  }

  public attached(param: SeriesAttachedParameter<Time, keyof SeriesOptionsMap>): void {
    this.requestUpdate = param.requestUpdate;
    this.bindEvents();
  }

  public detached(): void {
    this.showCrosshair();
    this.unbindEvents();
    this.requestUpdate = null;
  }

  public autoscaleInfo(_start: Logical, _end: Logical): AutoscaleInfo | null {
    return null;
  }

  public abstract getRenderData(): unknown; // todo: make proper type
  public abstract getState(): unknown;
  public abstract getSettingsTabs(): SettingsTab[];
  public abstract isCreationPending(): boolean;
  public abstract setState(state: unknown): void;

  public abstract hitTest(x: number, y: number): PrimitiveHoveredItem | null;
  public abstract updateAllViews(): void;
  public abstract paneViews(): readonly IPrimitivePaneView[];
  public abstract priceAxisPaneViews(): readonly IPrimitivePaneView[];
  public abstract priceAxisViews(): readonly ISeriesPrimitiveAxisView[];
  public abstract timeAxisPaneViews(): readonly IPrimitivePaneView[];
  public abstract timeAxisViews(): readonly ISeriesPrimitiveAxisView[];

  protected render(): void {
    this.updateAllViews();
    this.requestUpdate?.();
  }

  protected hideCrosshair(): void {
    this.chart.applyOptions({
      crosshair: {
        mode: CrosshairMode.Hidden,
      },
    });
  }

  protected showCrosshair(): void {
    this.chart.applyOptions({
      crosshair: {
        mode: CrosshairMode.Normal,
      },
    });
  }

  protected getEventPoint(event: PointerEvent): Point {
    return getPointerPointFromEvent(this.container, event);
  }

  protected bindEvents(): void {
    if (this.isBound) {
      return;
    }

    this.isBound = true;

    this.container.addEventListener('dblclick', this.handleDoubleClick);
    this.container.addEventListener('pointerdown', this.handlePointerDown);
    this.container.addEventListener('contextmenu', this.handleContextMenu);

    window.addEventListener('pointermove', this.handlePointerMove);
    window.addEventListener('pointerup', this.handlePointerUp);
    window.addEventListener('pointercancel', this.handlePointerUp);
  }

  protected unbindEvents(): void {
    if (!this.isBound) {
      return;
    }

    this.isBound = false;

    this.container.removeEventListener('dblclick', this.handleDoubleClick);
    this.container.removeEventListener('pointerdown', this.handlePointerDown);
    this.container.removeEventListener('contextmenu', this.handleContextMenu);

    window.removeEventListener('pointermove', this.handlePointerMove);
    window.removeEventListener('pointerup', this.handlePointerUp);
    window.removeEventListener('pointercancel', this.handlePointerUp);
  }

  // todo: хочется общую реализацию для каждой кнопки
  protected handleContextMenu(event: MouseEvent): void {}
  protected handleDoubleClick(event: MouseEvent): void {}
  protected handlePointerDown(event: PointerEvent): void {}
  protected handlePointerMove(event: PointerEvent): void {}
  protected handlePointerUp(event: PointerEvent): void {}

  protected abstract getGeometry(): unknown; // todo: make proper type
  protected abstract getTimeAxisSegments(): AxisSegment[];
  protected abstract getPriceAxisSegments(): AxisSegment[];
  protected abstract getTimeAxisLabel(kind: string): AxisLabel | null;
  protected abstract getPriceAxisLabel(kind: string): AxisLabel | null;
}


import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';

import { DOMObject, DOMObjectParams } from '@core/DOMObject';
import { ISeriesDrawing } from '@core/Drawings/common';
import { DrawingSnapshotItem } from '@core/DrawingsManager';
import { Hotkeys, Keys } from '@core/Hotkeys';
import { DrawingsNames } from '@src/constants';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { SettingsTab, SettingsValues } from '@src/types/settings';

type IDrawing = DOMObject;

interface DrawingParams extends DOMObjectParams {
  drawingName: DrawingsNames;
  lwcChart: IChartApi;
  mainSeries: SeriesStrategies;
  onDelete: (id: string) => void;
  construct: (chart: IChartApi, series: ISeriesApi<SeriesType>) => ISeriesDrawing;
  hotkeys: Hotkeys;
  setCopyPasteBuffer: (copiedObject: DrawingSnapshotItem) => void;
  resetActiveTool: () => void;
}

export class Drawing extends DOMObject implements IDrawing {
  private lwcDrawing: ISeriesDrawing;
  private mainSeries: SeriesStrategies;
  private drawingName: DrawingsNames;
  private hotkeys: Hotkeys;

  private escapeUnregisterHash: string | null = null;
  private deleteUnregisterHash: string | null = null;
  private copyUnregisterHash: string | null = null;

  constructor({
    lwcChart,
    name,
    mainSeries,
    drawingName,
    id,
    onDelete,
    zIndex,
    moveUp,
    moveDown,
    construct,
    paneId,
    hotkeys,
    setCopyPasteBuffer,
    resetActiveTool,
  }: DrawingParams) {
    super({ id, name, zIndex, onDelete, moveUp, moveDown, paneId });
    this.hotkeys = hotkeys;
    this.lwcDrawing = construct(lwcChart, mainSeries);
    this.onDelete = onDelete;
    this.escapeUnregisterHash = hotkeys.register({
      keys: [Keys.escape],
      callback: () => {
        this.delete();
        resetActiveTool();
      },
    });

    this.lwcDrawing.subscribeIsSelected((isSelected) => {
      if (isSelected) {
        this.deleteUnregisterHash = hotkeys.register({
          keys: [Keys.delete],
          callback: () => {
            this.delete();
          },
        });
        this.copyUnregisterHash = hotkeys.register({
          keys: [Keys.control, Keys.c],
          callback: () => {
            if (!this.isCreationPending()) {
              const copiedObject = {
                id: this.id,
                drawingName: this.getDrawingName(),
                state: this.getState(),
              };
              setCopyPasteBuffer(copiedObject);
            }
          },
        });
      } else {
        hotkeys.unregister({
          keys: [Keys.delete],
          hash: this.deleteUnregisterHash,
        });
        hotkeys.unregister({
          keys: [Keys.control, Keys.c],
          hash: this.copyUnregisterHash,
        });
      }
    });
    this.mainSeries = mainSeries;
    this.drawingName = drawingName;

    this.afterCreation(() => {
      hotkeys.unregister({
        keys: [Keys.escape],
        hash: this.escapeUnregisterHash,
      });
    });
  }

  public delete() {
    this.destroy();
    super.delete();
  }

  public getDrawingName(): DrawingsNames {
    return this.drawingName;
  }

  public getLwcDrawing() {
    return this.lwcDrawing;
  }

  public show() {
    this.lwcDrawing.show();
    super.show();
  }

  public hide() {
    this.lwcDrawing.hide();
    super.hide();
  }

  public rebind = (nextMainSeries: SeriesStrategies) => {
    this.lwcDrawing.rebind(nextMainSeries);
    this.mainSeries = nextMainSeries;
  };

  public isCreationPending(): boolean {
    return this.lwcDrawing.isCreationPending();
  }

  public async waitForCreation(): Promise<void> {
    return this.lwcDrawing.waitTillReady();
  }

  public shouldShowInObjectTree(): boolean {
    return this.lwcDrawing.shouldShowInObjectTree();
  }

  public getState(): unknown {
    return this.lwcDrawing.getState();
  }

  public setState(state: unknown): void {
    this.lwcDrawing.setState(state);
  }

  public getSettings(): SettingsValues {
    return this.lwcDrawing.getSettings();
  }

  public updateSettings(settings: SettingsValues): void {
    this.lwcDrawing.updateSettings(settings);
  }

  public getSettingsTabs(): SettingsTab[] {
    return this.lwcDrawing.getSettingsTabs();
  }

  public hasSettings(): boolean {
    return this.getSettingsTabs().some((tab) => tab.fields.length > 0);
  }

  public destroy() {
    this.hotkeys.unregister({
      keys: [Keys.delete],
      hash: this.deleteUnregisterHash,
    });
    this.hotkeys.unregister({
      keys: [Keys.escape],
      hash: this.escapeUnregisterHash,
    });
    this.hotkeys.unregister({
      keys: [Keys.control, Keys.c],
      hash: this.copyUnregisterHash,
    });
    this.mainSeries.detachPrimitive(this.lwcDrawing);
    this.lwcDrawing.destroy();
  }

  private async afterCreation(cb: () => void) {
    await this.lwcDrawing.waitTillReady();
    cb();
  }
}


import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';

import { EventManager } from '@core';
import { DOMModel } from '@core/DOMModel';
import { Drawing } from '@core/Drawings';
import { Hotkeys, Keys } from '@core/Hotkeys';

import { EntitySettingsModal } from '@src/components/EntitySettingsModal';
import { drawingLabelById, drawingsMap, DrawingsNames } from '@src/constants';
import { ModalRenderer } from '@src/core/ModalRenderer';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ActiveDrawingTool } from '@src/types';

interface DrawingsManagerParams {
  eventManager: EventManager;
  mainSeries$: Observable<SeriesStrategies | null>;
  lwcChart: IChartApi;
  DOM: DOMModel;
  container: HTMLElement;
  modalRenderer: ModalRenderer;
  paneId: number;
  hotkeys: Hotkeys;
}

export interface DrawingSnapshotItem {
  id: string;
  drawingName: DrawingsNames;
  state: unknown;
}

interface CreateDrawingOptions {
  id?: string;
  state?: unknown;
  shouldUpdateDrawingsList?: boolean;
}

export type DrawingsManagerSnapshot = DrawingSnapshotItem[];

export class DrawingsManager {
  private eventManager: EventManager;
  private lwcChart: IChartApi;
  private DOM: DOMModel;
  private container: HTMLElement;
  private modalRenderer: ModalRenderer;

  private mainSeries: SeriesStrategies | null = null;
  private subscriptions = new Subscription();
  private drawings$ = new BehaviorSubject<Drawing[]>([]);
  private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
  private endlessMode$ = new BehaviorSubject(false);
  private recreateScheduled = false;
  private pendingSnapshot: DrawingsManagerSnapshot | null = null;
  private paneId: number;

  private hotkeys: Hotkeys;
  private copyPasteBuffer: DrawingSnapshotItem | null = null;
  private escapeUnregisterHash: string | null = null;

  constructor({
    eventManager,
    mainSeries$,
    lwcChart,
    DOM,
    container,
    modalRenderer,
    paneId,
    hotkeys,
  }: DrawingsManagerParams) {
    this.DOM = DOM;
    this.eventManager = eventManager;
    this.paneId = paneId;
    this.lwcChart = lwcChart;
    this.container = container;
    this.modalRenderer = modalRenderer;
    this.hotkeys = hotkeys;

    this.subscriptions.add(
      mainSeries$.subscribe((series) => {
        if (!series) {
          return;
        }

        this.mainSeries = series;
        this.drawings$.value.forEach((drawing) => drawing.rebind(series));

        if (this.pendingSnapshot) {
          const snapshot = this.pendingSnapshot;
          this.pendingSnapshot = null;
          this.setSnapshot(snapshot);
        }
      }),
    );

    window.addEventListener('pointerup', this.handlePointerUp);
    this.container.addEventListener('click', this.handleClick);
    this.container.addEventListener('pointerdown', this.handlePointerDown);
    // todo: implement ctrl+v
    // hotkeys.register({
    //   keys: [Keys.control, Keys.v],
    //   callback: () => {
    //     const bufferWithAppliedPosition = {
    //       ...this.copyPasteBuffer,
    //       state: {
    //         ...this.copyPasteBuffer?.state,
    //         startAnchor: {
    //           price: 73.36210252637723,
    //           time: 1783679170
    //         }
    //       }
    //     }
    //
    //     this.setSnapshot([
    //       ...this.getSnapshot(),
    //       bufferWithAppliedPosition
    //     ])
    //     // hotkeys.unregister({ // todo: unregister all else ctrl+c's
    //     //   keys: [Keys.control, Keys.c]
    //     // })
    //   }
    // })
  }

  private handlePointerDown = (): void => {
    this.DOM.refreshEntities();
  };

  private handlePointerUp = (): void => {
    this.DOM.refreshEntities();
    this.updateActiveTool();
  };

  private handleClick = (): void => {
    this.DOM.refreshEntities();
    this.updateActiveTool();
  };

  private updateActiveTool = (): void => {
    const hasPendingDrawing = this.drawings$.value.some((drawing) => drawing.isCreationPending());

    if (hasPendingDrawing) {
      return;
    }

    const activeTool = this.activeTool$.value;
    const isSingleInstanceTool = activeTool !== 'crosshair' && drawingsMap[activeTool]?.singleInstance;

    if (activeTool !== 'crosshair' && this.endlessMode$.value && !isSingleInstanceTool) {
      if (this.recreateScheduled) {
        return;
      }

      this.recreateScheduled = true;

      queueMicrotask(() => {
        this.recreateScheduled = false;

        const currentTool = this.activeTool$.value;
        const hasPendingAfterTick = this.drawings$.value.some((drawing) => drawing.isCreationPending());

        if (currentTool === 'crosshair') {
          return;
        }

        if (!this.endlessMode$.value) {
          return;
        }

        if (drawingsMap[currentTool]?.singleInstance) {
          return;
        }

        if (hasPendingAfterTick) {
          return;
        }

        this.createDrawing(currentTool);
      });

      return;
    }

    this.activeTool$.next('crosshair');
  };

  private removeDrawing = (id: string): void => {
    const drawing = this.drawings$.value.find((item) => item.id === id);

    if (!drawing) {
      return;
    }

    this.removeDrawings([drawing]);
  };

  private removeDrawingsByName(name: DrawingsNames, shouldUpdateTool = true): void {
    const drawingsToRemove = this.drawings$.value.filter((drawing) => drawing.getDrawingName() === name);

    this.removeDrawings(drawingsToRemove, shouldUpdateTool);
  }

  private removePendingDrawings(shouldUpdateTool = true): void {
    const drawingsToRemove = this.drawings$.value.filter((drawing) => drawing.isCreationPending());

    this.removeDrawings(drawingsToRemove, shouldUpdateTool);
  }

  private removeDrawings(drawingsToRemove: Drawing[], shouldUpdateTool = true): void {
    if (!drawingsToRemove.length) {
      return;
    }

    drawingsToRemove.forEach((drawing) => {
      drawing.destroy();
      this.DOM.removeEntity(drawing);
    });

    this.drawings$.next(this.drawings$.value.filter((drawing) => !drawingsToRemove.includes(drawing)));

    if (shouldUpdateTool) {
      this.updateActiveTool();
    }

    this.DOM.refreshEntities();
  }

  public addDrawingForce = (name: DrawingsNames): Promise<void> => {
    this.removePendingDrawings(false);

    if (drawingsMap[name].singleInstance) {
      this.removeDrawingsByName(name, false);
    }

    this.activeTool$.next(name);
    const drawing = this.createDrawing(name);
    this.DOM.refreshEntities();

    return drawing.waitForCreation();
  };

  private createDrawing(name: DrawingsNames, options: CreateDrawingOptions = {}): Drawing {
    if (!this.mainSeries) {
      throw new Error('[Drawings] main series is not defined');
    }

    const { id, state, shouldUpdateDrawingsList = true } = options;

    const config = drawingsMap[name];
    const drawingId = id ?? crypto.randomUUID();

    let createdDrawing: Drawing | null = null;

    const construct = (chart: IChartApi, series: ISeriesApi<SeriesType>) => {
      const paneElement = series.getPane().getHTMLElement();

      if (!paneElement) {
        throw new Error('[Drawing Manager]: cannot place drawing, there is no pane');
      }

      const cells = paneElement.querySelectorAll<HTMLTableCellElement>(':scope > td');
      const canvasElement = cells.item(1);

      return config.construct({
        chart,
        series,
        eventManager: this.eventManager,
        container: canvasElement,
        removeSelf: () => this.removeDrawing(drawingId),
        openSettings: () => {
          if (createdDrawing) {
            this.openSettings(createdDrawing);
          }
        },
      });
    };

    const drawingFactory = (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) =>
      new Drawing({
        lwcChart: this.lwcChart,
        mainSeries: this.mainSeries as SeriesStrategies,
        id: drawingId,
        drawingName: name,
        name: drawingLabelById()[name],
        onDelete: this.removeDrawing,
        zIndex,
        moveDown,
        moveUp,
        construct,
        paneId: this.paneId,
        hotkeys: this.hotkeys,
        setCopyPasteBuffer: (copyPasteBuffer) => {
          this.copyPasteBuffer = copyPasteBuffer;
        },
        resetActiveTool: () => {
          this.activeTool$.next('crosshair');
        },
      });

    const entity = this.DOM.setEntity<Drawing>(drawingFactory);
    createdDrawing = entity;

    if (state !== undefined) {
      entity.setState(state);
    }

    if (shouldUpdateDrawingsList) {
      this.drawings$.next([...this.drawings$.value, entity]);
    }

    return entity;
  }

  public getSnapshot(): DrawingsManagerSnapshot {
    return this.drawings$.value
      .filter((drawing) => !drawing.isCreationPending())
      .map((drawing) => ({
        id: drawing.id,
        drawingName: drawing.getDrawingName(),
        state: drawing.getState(),
      }));
  }

  public setSnapshot(snapshot: DrawingsManagerSnapshot): void {
    if (!Array.isArray(snapshot)) {
      return;
    }

    if (!this.mainSeries) {
      this.pendingSnapshot = snapshot;
      return;
    }

    this.removeDrawings(this.drawings$.value, false);

    const restoredDrawings = snapshot.reduce<Drawing[]>((drawings, item) => {
      if (!drawingsMap[item.drawingName]) {
        return drawings;
      }

      drawings.push(
        this.createDrawing(item.drawingName, { id: item.id, state: item.state, shouldUpdateDrawingsList: false }),
      );

      return drawings;
    }, []);

    this.drawings$.next(restoredDrawings);
    this.activeTool$.next('crosshair');
    this.DOM.refreshEntities();
  }

  public setEndlessDrawingMode = (value: boolean): void => {
    if (value) {
      this.escapeUnregisterHash = this.hotkeys.register({
        keys: [Keys.escape],
        callback: () => {
          this.setEndlessDrawingMode(false);
        },
      });
    } else {
      this.hotkeys.unregister({
        keys: [Keys.escape],
        hash: this.escapeUnregisterHash,
      });
    }
    this.endlessMode$.next(value);
  };

  public isEndlessDrawingsMode(): Observable<boolean> {
    return this.endlessMode$.asObservable();
  }

  public getActiveTool(): Observable<ActiveDrawingTool> {
    return this.activeTool$.asObservable();
  }

  public activateCrosshair(): void {
    this.removePendingDrawings(false);
    this.activeTool$.next('crosshair');
    this.DOM.refreshEntities();
  }

  public entities(): Observable<Drawing[]> {
    return this.drawings$.asObservable();
  }

  private openSettings = (drawing: Drawing) => {
    const tabs = drawing.getSettingsTabs();

    if (!tabs.length || tabs.every((tab) => tab.fields.length === 0)) {
      return;
    }

    let settings = drawing.getSettings();

    this.modalRenderer.renderComponent(
      <EntitySettingsModal
        tabs={tabs}
        values={settings}
        onChange={(nextSettings) => {
          settings = nextSettings;
        }}
        initialTabKey={tabs[0]?.key}
      />,
      {
        size: 'sm',
        title: drawing.name,
        onSave: () => drawing.updateSettings(settings),
      },
    );
  };

  public getDrawings(): Drawing[] {
    return this.drawings$.value;
  }

  public hideAll(): void {
    this.drawings$.value.forEach((drawing) => drawing.hide());
    this.DOM.refreshEntities();
  }

  public destroy(): void {
    this.hotkeys.unregister({
      keys: [Keys.escape],
      hash: this.escapeUnregisterHash,
    });
    window.removeEventListener('pointerup', this.handlePointerUp);
    this.container.removeEventListener('click', this.handleClick);
    this.container.removeEventListener('pointerdown', this.handlePointerDown);

    this.drawings$.value.forEach((drawing) => drawing.destroy());

    this.subscriptions.unsubscribe();
    this.drawings$.complete();
    this.activeTool$.complete();
    this.endlessMode$.complete();
  }
}
import { AxisLine } from '@src/core/Drawings/axisLine';
import { Diapson } from '@src/core/Drawings/diapson';
import { FibonacciRetracement } from '@src/core/Drawings/fibonacciRetracement';
import { Ray } from '@src/core/Drawings/ray';
import { Rectangle } from '@src/core/Drawings/rectangle';
import { Ruler } from '@src/core/Drawings/ruler';
import { SliderPosition } from '@src/core/Drawings/sliderPosition';
import { Text } from '@src/core/Drawings/text';
import { Traectory } from '@src/core/Drawings/traectory';
import { TrendLine } from '@src/core/Drawings/trendLine';
import { VolumeProfile } from '@src/core/Drawings/volumeProfile';
import { t } from '@src/translations';
import { DrawingConfig } from '@src/types';

export enum DrawingsNames {
  'trendLine' = 'trendLine',
  'ray' = 'ray',
  'horizontalLine' = 'horizontalLine',
  'horizontalRay' = 'horizontalRay',
  'verticalLine' = 'verticalLine',
  'ruler' = 'ruler',

  'fibonacciRetracement' = 'fibonacciRetracement',

  'sliderLong' = 'sliderLong',
  'sliderShort' = 'sliderShort',
  'diapsonDates' = 'diapsonDates',
  'diapsonPrices' = 'diapsonPrices',
  'fixedRangeProfile' = 'fixedRangeProfile',
  'visibleRangeProfile' = 'visibleRangeProfile',

  'rectangle' = 'rectangle',
  'traectory' = 'traectory',

  'text' = 'text',
}

export const drawingLabelById = (): Record<DrawingsNames, string> => ({
  [DrawingsNames.trendLine]: t('Trend line'),
  [DrawingsNames.ray]: t('Ray'),
  [DrawingsNames.horizontalLine]: t('Horizontal line'),
  [DrawingsNames.horizontalRay]: t('Horizontal ray'),
  [DrawingsNames.verticalLine]: t('Vertical line'),
  [DrawingsNames.fibonacciRetracement]: t('Fibonacci retracement'),
  [DrawingsNames.ruler]: t('Ruler'),
  [DrawingsNames.sliderLong]: t('Long position'),
  [DrawingsNames.sliderShort]: t('Short position'),
  [DrawingsNames.diapsonDates]: t('Dates range'),
  [DrawingsNames.diapsonPrices]: t('Prices range'),
  [DrawingsNames.fixedRangeProfile]: t('Fixed range profile volume'),
  [DrawingsNames.visibleRangeProfile]: t('Anchored volume profile'),
  [DrawingsNames.rectangle]: t('Rectangle'),
  [DrawingsNames.traectory]: t('Traectory'),
  [DrawingsNames.text]: t('Text'),
});

export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
  [DrawingsNames.trendLine]: {
    construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
      return new TrendLine(chart, series, {
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.ray]: {
    construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
      return new Ray(chart, series, {
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.horizontalLine]: {
    construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
      return new AxisLine(chart, series, {
        direction: 'horizontal',
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.horizontalRay]: {
    construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
      return new TrendLine(chart, series, {
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.verticalLine]: {
    construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
      return new AxisLine(chart, series, {
        direction: 'vertical',
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.sliderLong]: {
    construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
      return new SliderPosition(chart, series, {
        side: 'long',
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.fibonacciRetracement]: {
    construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
      return new FibonacciRetracement(chart, series, {
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.sliderShort]: {
    construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
      return new SliderPosition(chart, series, {
        side: 'short',
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.diapsonDates]: {
    construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
      return new Diapson(chart, series, {
        rangeMode: 'date',
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.diapsonPrices]: {
    construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
      return new Diapson(chart, series, {
        rangeMode: 'price',
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.fixedRangeProfile]: {
    construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
      return new VolumeProfile(chart, series, {
        profileKind: 'fixedRange',
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.visibleRangeProfile]: {
    singleInstance: true,
    construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
      return new VolumeProfile(chart, series, {
        profileKind: 'visibleRange',
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.rectangle]: {
    construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
      return new Rectangle(chart, series, {
        container,
        formatObservable: eventManager.getChartOptionsModel(),
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.ruler]: {
    singleInstance: true,
    construct: ({ chart, series, eventManager, container, removeSelf }) => {
      return new Ruler(chart, series, {
        formatObservable: eventManager.getChartOptionsModel(),
        container,
        resetTriggers: [eventManager.getTimeframeObs(), eventManager.getInterval()],
        removeSelf,
      });
    },
  },
  [DrawingsNames.traectory]: {
    construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
      return new Traectory(chart, series, {
        formatObservable: eventManager.getChartOptionsModel(),
        container,
        removeSelf,
        openSettings,
      });
    },
  },
  [DrawingsNames.text]: {
    construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
      return new Text(chart, series, {
        formatObservable: eventManager.getChartOptionsModel(),
        container,
        removeSelf,
        openSettings,
      });
    },
  },
};


import { THEME_KEY, THEME_MODE } from './constants';
import { hexWithAplha } from './helpers';
import { ThemePalette } from './types';

export const THEME_PALETTE: ThemePalette = {
  [THEME_KEY.MXT]: {
    [THEME_MODE.LIGHT]: {
      neutral0: '#ffffff',
      neutral1Inv: '#fbfcfe',
      neutral2: '#f8f9fa',
      neutral2Inv: 'f8f9fa',
      neutral4: '#e7e8e8',
      neutral5: '#dfe0e4',
      neutral7: '#cececf',
      neutral12: '#202020',
      neutral12Inv: '#202020',
      neutral13: '#000000',
      neutral13Inv: hexWithAplha('#000000', 0.8),
      blue1: '#4865ef',
      red1: '#d71d38',
      orange1: '#ef623a',
      blue2: hexWithAplha('#4865ef', 0.2),
      red2: hexWithAplha('#d71d38', 0.2),
      red4: hexWithAplha('#d71d38', 0.4),
      red5: hexWithAplha('#d71d38', 0.2),
      green1: '#24a249',
      green4: hexWithAplha('#24a249', 0.4),
      green5: hexWithAplha('#24a249', 0.2),
      magenta1: '#aa01ff',
      magenta2: hexWithAplha('#aa01ff', 0.2),
      teal2: hexWithAplha('#26c4d9', 0.8),
      teal3: hexWithAplha('#24a249', 0.1),
      pink2: hexWithAplha('#e05287', 0.8),
      gray5: '#dfe0e4',
      gray10: '#818182',
      gray11: '#646465',
      gray12: '#202020',
      contrast1: '#ffffff',
      contrast2: hexWithAplha('#ffffff', 0.84),
    },
  },
  [THEME_KEY.MB]: {
    [THEME_MODE.LIGHT]: {
      neutral0: '#ffffff',
      neutral1Inv: '#fcfcfd',
      neutral2: '#f9f9fb',
      neutral2Inv: '#f9f9fb',
      neutral4: '#e7E8EC',
      neutral5: '#E0E1E6',
      neutral7: '#cdced7',
      neutral12: '#1E1F24',
      neutral12Inv: '#1e1f24',
      neutral13: '#000000',
      neutral13Inv: hexWithAplha('#000000', 0.8),
      blue1: '#4c69e7',
      red1: '#d71d38',
      orange1: '#e7613f',
      blue2: hexWithAplha('#4c69e7', 0.2),
      red2: hexWithAplha('#d71d38', 0.2),
      red4: hexWithAplha('#d71d38', 0.4),
      red5: hexWithAplha('#d71d38', 0.2),
      green1: '#3cae0b',
      green4: hexWithAplha('#3cae0b', 0.4),
      green5: hexWithAplha('#3cae0b', 0.2),
      magenta1: '#aa01ff',
      magenta2: hexWithAplha('#aa01ff', 0.2),
      teal2: hexWithAplha('#26c4d9', 0.8),
      teal3: hexWithAplha('#3cae0b', 0.1),
      pink2: hexWithAplha('#e05287', 0.8),
      gray5: '#e0e1e6',
      gray10: '#80828d',
      gray11: '#62636c',
      gray12: '#1e1f24',
      contrast1: '#ffffff',
      contrast2: hexWithAplha('#ffffff', 0.84),
    },
    [THEME_MODE.DARK]: {
      neutral0: '#000000',
      neutral2: '#19191b',
      neutral3: hexWithAplha('#222325', 0.8),
      neutral4: '#292A2E',
      neutral5: '#303136',
      neutral7: '#46484f',
      neutral12: '#EEEEF0',
      neutral13: '#FFFFFF',
      blue1: '#4c69e7',
      red1: '#d71d38',
      orange1: '#e7613f',
      blue2: hexWithAplha('#4c69e7', 0.4),
      red2: hexWithAplha('#d71d38', 0.2),
      red4: hexWithAplha('#d71d38', 0.4),
      red5: hexWithAplha('#d71d38', 0.2),
      green1: '#3cae0b',
      green4: hexWithAplha('#3cae0b', 0.4),
      green5: hexWithAplha('#3cae0b', 0.2),
      magenta1: '#aa01ff',
      magenta2: hexWithAplha('#aa01ff', 0.2),
      teal2: hexWithAplha('#26c4d9', 0.8),
      teal3: hexWithAplha('#3cae0b', 0.2),
      pink2: hexWithAplha('#e05287', 0.8),
      gray5: '#303136',
      gray10: '#797b86',
      gray11: '#b2b3bd',
      gray12: '#eeeef0',
      gray12Inv: '#303136',
      contrast1: '#ffffff',
      contrast2: hexWithAplha('#ffffff', 0.84),
    },
  },
  [THEME_KEY.TR]: {
    [THEME_MODE.DARK]: {
      neutral0: '#111112',
      neutral2: hexWithAplha('#3d3d5c', 0.52),
      neutral3: '#21212c',
      neutral4: hexWithAplha('#5041ff', 0.28),
      neutral5: hexWithAplha('#5041ff', 0.52),
      neutral7: hexWithAplha('#3d3d5c', 0.52),
      neutral12: hexWithAplha('#eeeef0', 0.52),
      neutral13: hexWithAplha('#eeeef0', 0.84),
      blue1: '#5041ff',
      red1: hexWithAplha('#ff0000', 0.52),
      blue2: hexWithAplha('#5041ff', 0.4),
      red2: hexWithAplha('#ff0000', 0.2),
      red4: hexWithAplha('#ff0000', 0.2),
      red5: hexWithAplha('#ff0000', 0.2),
      orange1: '#ff5a33',
      green1: hexWithAplha('#27ef00', 0.52),
      green4: hexWithAplha('#27ef00', 0.2),
      green5: hexWithAplha('#27ef00', 0.2),
      magenta1: '#aa01ff',
      magenta2: hexWithAplha('#aa01ff', 0.2),
      teal2: hexWithAplha('#4db2b2', 0.8),
      teal3: hexWithAplha('#27ef00', 0.2),
      pink2: hexWithAplha('#b24ca1', 0.8),
      gray5: hexWithAplha('#3d3d5c', 0.28),
      gray10: hexWithAplha('#ffffff', 0.68),
      gray11: hexWithAplha('#ffffff', 0.52),
      gray12: hexWithAplha('#ffffff', 0.84),
      gray12Inv: hexWithAplha('#3d3d5c', 0.68),
      contrast1: '#ffffff',
      contrast2: hexWithAplha('#ffffff', 0.84),
    },
  },
} as const;

export const INDICATOR_COLOR_PALETTE = [
  '#56A8FF',
  '#4A90FF',
  '#4F79FF',
  '#6A63FF',
  '#8B5FFF',
  '#AF5DF8',
  '#D45FEE',
  '#F062DB',
  '#FF67BB',
  '#FF6F8F',
  '#FF8A4A',
  '#F5A43C',
  '#E7BE44',
  '#D5D44E',
  '#B8DB58',
  '#96D663',
  '#74CF73',
  '#5FCB97',
  '#67D4BF',
  '#79D8E2',
] as const;

export const COMPARE_COLOR_PALETTE = [
  '#F9D423',
  '#4FACFE',
  '#00e676',
  '#FF5252',
  '#B388FF',
  '#FFAB40',
  '#40c4FF',
  '#ff4081',
] as const;

export const INDICATOR_COLOR_PALETTE_MIDDLE_INDEX = Math.floor(INDICATOR_COLOR_PALETTE.length / 2);
import { DEFAULT_THEME_MODE, THEME_KEY, THEME_MODE } from './constants';
import { hexWithAplha } from './helpers';
import { THEME_PALETTE } from './palette';
import { ComponentColorTokens, ThemeKey, ThemeMap, ThemeMode, ThemeModes, ThemeOptions } from './types';

const commonColors = (theme: ThemeKey, mode: ThemeMode) => {
  const palette = getThemePalette({ theme, mode });

  return {
    chartBackground: palette.neutral0,
    chartTextPrimary: palette.gray11,
    chartGridLine: palette.gray5,
    chartBorder: palette.gray5,
    chartCandleUp: palette.green1,
    chartCandleWickUp: palette.green4,
    chartCandleDown: palette.red1,
    chartCandleWickDown: palette.red4,
    chartLineColor: palette.blue1,
    chartLineColorAlternative: palette.red1,
    chartCrosshairLine: palette.gray10,
    chartPriceLineText: palette.contrast1,
    indicatorLineSma: palette.red1,
    indicatorLineEma: palette.orange1,
    axisMarkerLabelFill: palette.blue1,
    axisMarkerAreaFill: palette.blue2,
    axisMarkerLabelDefaultFill: palette.gray10,
    axisMarkerLabelPositiveFill: palette.green1,
    axisMarkerLabelNegativeFill: palette.red1,
    rulerPositiveFill: palette.blue2,
    rulerNegativeFill: palette.red2,
    sliderPositiveFill: palette.green5,
    sliderNegativeFill: palette.red5,
    rectangleBorderFill: palette.magenta1,
    rectangleAreaFill: palette.magenta2,
    diapsonStrokeFill: palette.blue1,
    diapsonAreaFill: palette.blue2,
    axisRangeTooltipText: palette.contrast2,
    volumeProfileAreaFill: palette.teal3,
    volumeProfilePocLine: palette.gray12,
    volumeProfileBuyFill: palette.teal2,
    volumeProfileSellFill: palette.pink2,
  };
};

export const THEME: ThemeMap = {
  [THEME_KEY.MXT]: {
    [THEME_MODE.LIGHT]: {
      ...commonColors(THEME_KEY.MXT, THEME_MODE.LIGHT),
      chartCrosshairLabel: THEME_PALETTE[THEME_KEY.MXT][THEME_MODE.LIGHT].gray12,
      axisRangeTooltipFill: THEME_PALETTE[THEME_KEY.MXT][THEME_MODE.LIGHT].gray12,
      chartTooltipBackground: hexWithAplha(THEME_PALETTE[THEME_KEY.MXT][THEME_MODE.LIGHT].neutral13, 0.8),
    },
  },
  [THEME_KEY.MB]: {
    [THEME_MODE.LIGHT]: {
      ...commonColors(THEME_KEY.MB, THEME_MODE.LIGHT),
      chartCrosshairLabel: THEME_PALETTE[THEME_KEY.MB][THEME_MODE.LIGHT].gray12,
      axisRangeTooltipFill: THEME_PALETTE[THEME_KEY.MB][THEME_MODE.LIGHT].gray12,
      chartTooltipBackground: hexWithAplha(THEME_PALETTE[THEME_KEY.MB][THEME_MODE.LIGHT].neutral13, 0.8),
    },
    [THEME_MODE.DARK]: {
      ...commonColors(THEME_KEY.MB, THEME_MODE.DARK),
      chartCrosshairLabel: THEME_PALETTE[THEME_KEY.MB][THEME_MODE.DARK].gray12Inv,
      axisRangeTooltipFill: THEME_PALETTE[THEME_KEY.MB][THEME_MODE.DARK].gray12Inv,
      chartTooltipBackground: hexWithAplha(THEME_PALETTE[THEME_KEY.MB][THEME_MODE.DARK].neutral3, 0.8),
    },
  },
  [THEME_KEY.TR]: {
    [THEME_MODE.DARK]: {
      ...commonColors(THEME_KEY.TR, THEME_MODE.DARK),
      chartCrosshairLabel: THEME_PALETTE[THEME_KEY.TR][THEME_MODE.DARK].gray12Inv,
      axisRangeTooltipFill: THEME_PALETTE[THEME_KEY.TR][THEME_MODE.DARK].gray12Inv,
      chartTooltipBackground: THEME_PALETTE[THEME_KEY.TR][THEME_MODE.DARK].neutral3,
    },
  },
} as const;

export function getThemeColors<K extends ThemeKey, M extends ThemeModes[K]>({
  theme,
  mode,
}: ThemeOptions<K, M>): Record<ComponentColorTokens, string> {
  const themeObj = THEME[theme];
  const m = (mode ?? DEFAULT_THEME_MODE[theme]) as keyof typeof themeObj;

  return themeObj[m];
}

function getThemePalette<K extends ThemeKey, M extends ThemeModes[K]>({
  theme,
  mode,
}: ThemeOptions<K, M>): Record<string, string> {
  const paletteObj = THEME_PALETTE[theme];
  const m = (mode ?? DEFAULT_THEME_MODE[theme]) as keyof typeof paletteObj;

  return paletteObj[m];
}