<commit_before>import { Rectangle, pt } from "lively.graphics";
import { arr } from "lively.lang";
import { inspect } from "lively.morphic";
import { Range } from "../text/range.js";

function todo(name) { throw new Error("not yet implemented " + name)}

export default class TextLayout {

  constructor() {
    this.reset();
  }

  reset() {
    this.resetLineCharBoundsCache();
  }

  resetLineCharBoundsCache(morph) {
    this.lineCharBoundsCache = new WeakMap();
    if (morph) {
      this.estimateLineHeights(morph, true);
      morph.makeDirty();
    }
  }

  resetLineCharBoundsCacheOfLine(line) {
    if (!line) return;
    this.lineCharBoundsCache.set(line, null);
  }

  resetLineCharBoundsCacheOfRow(morph, row) {
    let doc = morph.document;
    doc && this.resetLineCharBoundsCacheOfLine(doc.getLine(row));
  }

  resetLineCharBoundsCacheOfRange(morph, range) {
    for (let row = range.start.row; row <= range.end.row; row++)
      this.resetLineCharBoundsCacheOfRow(morph, row);
  }

  estimateLineHeights(morph, force = false) {
    let {
          fontMetric, textRenderer, document,
          defaultTextStyle,
          extent: {x: morphWidth, y: morphHeight},
          clipMode, textAlign, padding, lineWrapping
        } = morph,
        paddingLeft = padding.left(),
        paddingRight = padding.right(),
        paddingTop = padding.top(),
        paddingBottom = padding.bottom(),
        { lines, stringSize } = document;

    // fast and exact version for small texts:
    if (morph.lineCount() < 10 && morph.document.stringSize < 3000) {
      var directRenderLineFn = textRenderer.directRenderLineFn(morph),
          directRenderTextLayerFn = textRenderer.directRenderTextLayerFn(morph),
          linesBounds = fontMetric.manuallyComputeBoundsOfLines(
            lines, 0, 0, {
              defaultTextStyle, width: morphWidth, height: morphHeight,
              clipMode, lineWrapping, textAlign,
              paddingLeft, paddingRight, paddingTop, paddingBottom
            }, directRenderTextLayerFn, directRenderLineFn);
      for (var i = 0; i < lines.length; i++) {
        var {width, height} = linesBounds[i];
        lines[i].changeExtent(width, height, false);
      }
      return;
    }

    var directRenderTextLayerFn = textRenderer.directRenderTextLayerFn(morph);

    morphWidth = morphWidth - paddingLeft - paddingRight;

    for (var i = 0; i < lines.length; i++) {
      var line = lines[i];
      if (!force && line.height > 0) continue;

      var textAttributes = line.textAttributes, styles = [];

      // find all styles that apply to line
      if (!textAttributes || !textAttributes.length) styles.push(defaultTextStyle);
      else for (var j = 0; j < textAttributes.length; j++)
        styles.push({...defaultTextStyle, ...textAttributes[j]});

      // measure default char widths and heights
      var measureCount = styles.length, // for avg width
          charWidthSum = 0,
          charHeight = 0;
      for (var h = 0; h < measureCount; h++) {
        var {width, height} = fontMetric.defaultCharExtent({
          defaultTextStyle: styles[h],
          width: 1000,
          paddingBottom, paddingTop, paddingRight, paddingLeft
        }, directRenderTextLayerFn);
        charHeight = Math.max(height, charHeight);
        charWidthSum = charWidthSum + width;
      }

      var estimatedHeight = charHeight,
          charCount = line.text.length || 1,
          charWidth = Math.round(charWidthSum/measureCount),
          unwrappedWidth = charCount * charWidth,
          estimatedWidth = !lineWrapping ? unwrappedWidth : Math.min(unwrappedWidth, morphWidth);
      if (lineWrapping) {
        var charsPerline = Math.max(3, morphWidth / charWidth),
            wrappedLineCount = Math.ceil(charCount / charsPerline) || 1,
        estimatedHeight = Math.round(wrappedLineCount * charHeight);
      }
      line.changeExtent(estimatedWidth, estimatedHeight, true);
    }

    morph.viewState._textLayoutStale = false;
  }

  defaultCharExtent(morph) {
    let {textRenderer, fontMetric, defaultTextStyle} = morph,
        directRenderTextLayerFn = textRenderer.directRenderTextLayerFn(morph);
    return fontMetric.defaultCharExtent({defaultTextStyle, width: 1000}, directRenderTextLayerFn);
  }

  textBounds(morph) {
    let {
      document: doc,
      padding,
      borderWidthLeft,
      borderWidthRight,
      borderWidthTop,
      borderWidthBottom<commit_msg>add text layout debugging (estimate line extent)<commit_after>import { Rectangle, pt } from "lively.graphics";
import { arr } from "lively.lang";
import { inspect } from "lively.morphic";
import { Range } from "../text/range.js";

function todo(name) { throw new Error("not yet implemented " + name)}

export default class TextLayout {

  constructor() {
    this.reset();
  }

  reset() {
    this.resetLineCharBoundsCache();
  }

  resetLineCharBoundsCache(morph) {
    this.lineCharBoundsCache = new WeakMap();
    if (morph) {
      this.estimateLineHeights(morph, true);
      morph.makeDirty();
    }
  }

  resetLineCharBoundsCacheOfLine(line) {
    if (!line) return;
    this.lineCharBoundsCache.set(line, null);
  }

  resetLineCharBoundsCacheOfRow(morph, row) {
    let doc = morph.document;
    doc && this.resetLineCharBoundsCacheOfLine(doc.getLine(row));
  }

  resetLineCharBoundsCacheOfRange(morph, range) {
    for (let row = range.start.row; row <= range.end.row; row++)
      this.resetLineCharBoundsCacheOfRow(morph, row);
  }

  estimateLineHeights(morph, force = false) {
    let {
          fontMetric, textRenderer, document,
          defaultTextStyle,
          extent: {x: morphWidth, y: morphHeight},
          clipMode, textAlign, padding, lineWrapping,
          debug
        } = morph,
        paddingLeft = padding.left(),
        paddingRight = padding.right(),
        paddingTop = padding.top(),
        paddingBottom = padding.bottom(),
        { lines, stringSize } = document;

    if (debug) {
      debug = morph.debugHelper(debug);
      if (!debug.debugTextLayout) debug = false;
    }

    // fast and exact version for small texts:
    if (!debug && morph.lineCount() < 10 && morph.document.stringSize < 3000) {
      var directRenderLineFn = textRenderer.directRenderLineFn(morph),
          directRenderTextLayerFn = textRenderer.directRenderTextLayerFn(morph),
          linesBounds = fontMetric.manuallyComputeBoundsOfLines(
            lines, 0, 0, {
              defaultTextStyle, width: morphWidth, height: morphHeight,
              clipMode, lineWrapping, textAlign,
              paddingLeft, paddingRight, paddingTop, paddingBottom
            }, directRenderTextLayerFn, directRenderLineFn);
      for (var i = 0; i < lines.length; i++) {
        var {width, height} = linesBounds[i];
        lines[i].changeExtent(width, height, false);
      }
      return;
    }

    var directRenderTextLayerFn = textRenderer.directRenderTextLayerFn(morph),
        nMeasured = 0;

    morphWidth = morphWidth - paddingLeft - paddingRight;

    for (var i = 0; i < lines.length; i++) {
      var line = lines[i];
      if (!force && line.height > 0) continue;

      nMeasured++;
      var textAttributes = line.textAttributes, styles = [];

      // find all styles that apply to line
      if (!textAttributes || !textAttributes.length) styles.push(defaultTextStyle);
      else for (var j = 0; j < textAttributes.length; j++)
        styles.push({...defaultTextStyle, ...textAttributes[j]});

      // measure default char widths and heights
      var measureCount = styles.length, // for avg width
          charWidthSum = 0,
          charHeight = 0;
      for (var h = 0; h < measureCount; h++) {
        var {width, height} = fontMetric.defaultCharExtent({
          defaultTextStyle: styles[h],
          width: 1000,
          paddingBottom, paddingTop, paddingRight, paddingLeft
        }, directRenderTextLayerFn);
        charHeight = Math.max(height, charHeight);
        charWidthSum = charWidthSum + width;
      }

      var estimatedHeight = charHeight,
          charCount = line.text.length || 1,
          charWidth = Math.round(charWidthSum / measureCount),
          unwrappedWidth = charCount * charWidth,
          estimatedWidth = !lineWrapping ? unwrappedWidth : Math.min(unwrappedWidth, morphWidth);
      if (lineWrapping) {
        var charsPerline = Math.max(3, morphWidth / charWidth),
            wrappedLineCount = Math.ceil(charCount / charsPerline) || 1,
            estimatedHeight = Math.round(wrappedLineCount * charHeight);
      }
      debug && debug.log(`${line.row}: ${line.height} => ${estimatedHeight}`)
      line.changeExtent(estimatedWidth, estimatedHeight, true);
    }

    morph.viewState._textLayoutStale = false;

    debug && debug.log(`estimateLineHeights, ${nMeasured}/${lines.length} updated`)
  }

  defaultCharExtent(morph) {
    let {textRenderer, fontMetric, defaultTextStyle} = morph,
        directRenderTextLayerFn = textRenderer.directRenderTextLayerFn(morph);
    return fontMetric.defaultCharExtent({defaultTextStyle, width: 1000}, directRenderTextLayerFn);
  }

  textBounds(morph) {
    let {
      document: doc,
      padding,
      borderWidthLeft,
      borderWidthRight,
      borderWidthTop,
      borderWidthBottom
{'fraction_non_alphanumeric': 0.11016584105104459, 'fraction_numerical': 0.005707516691794099, 'mean_word_length': 2.8744263662911975, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 6, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '863601', 'n_tokens_mistral': 2734, 'n_tokens_neox': 2651, 'n_words': 611}