<commit_before>// Copyright 2021-2022, University of Colorado Boulder

/**
 * Main flow-layout logic. Usually used indirectly through FlowBox, but can also be used directly (say, if nodes don't
 * have the same parent, or a FlowBox can't be used).
 *
 * @author <NAME> <<EMAIL>>
 */

import Bounds2 from '../../../../dot/js/Bounds2.js';
import Orientation from '../../../../phet-core/js/Orientation.js';
import arrayRemove from '../../../../phet-core/js/arrayRemove.js';
import mutate from '../../../../phet-core/js/mutate.js';
import { Divider, ExternalFlowConfigurableOptions, FLOW_CONFIGURABLE_OPTION_KEYS, FlowCell, FlowConfigurable, FlowLine, HorizontalLayoutJustification, LayoutAlign, LayoutJustification, Node, NodeLayoutAvailableConstraintOptions, NodeLayoutConstraint, scenery, VerticalLayoutJustification } from '../../imports.js';
import IProperty from '../../../../axon/js/IProperty.js';

const FLOW_CONSTRAINT_OPTION_KEYS = [
  ...FLOW_CONFIGURABLE_OPTION_KEYS,
  'spacing',
  'lineSpacing',
  'justify',
  'justifyLines',
  'wrap',
  'excludeInvisible'
];

type SelfOptions = {
  // The default spacing in-between elements in the primary direction. If additional (or less) spacing is desired for
  // certain elements, per-element margins (even negative) can be set in the layoutOptions of nodes contained.
  spacing?: number;

  // The default spacing in-between lines in the secondary direction.
  lineSpacing?: number;

  // How extra space in the primary direction is allocated. The default is spaceBetween.
  justify?: HorizontalLayoutJustification | VerticalLayoutJustification;

  // How extra space in the secondary direction is allocated. The default is null (which will expand content to fit)
  justifyLines?: HorizontalLayoutJustification | VerticalLayoutJustification | null;

  // Whether line-wrapping is enabled. If so, the primary preferred dimension will determine where things are wrapped.
  wrap?: boolean;

  // The preferred width/height (ideally from a container's localPreferredWidth/localPreferredHeight.
  preferredWidthProperty?: IProperty<number | null>;
  preferredHeightProperty?: IProperty<number | null>;

  // The minimum width/height (ideally from a container's localMinimumWidth/localMinimumHeight.
  minimumWidthProperty?: IProperty<number | null>;
  minimumHeightProperty?: IProperty<number | null>;
};

export type FlowConstraintOptions = SelfOptions & ExternalFlowConfigurableOptions & NodeLayoutAvailableConstraintOptions;

export default class FlowConstraint extends FlowConfigurable( NodeLayoutConstraint ) {

  private readonly cells: FlowCell[] = [];
  private _justify: LayoutJustification = LayoutJustification.SPACE_BETWEEN;
  private _justifyLines: LayoutJustification | null = null;
  private _wrap = false;
  private _spacing = 0;
  private _lineSpacing = 0;

  constructor( ancestorNode: Node, providedOptions?: FlowConstraintOptions ) {
    assert && assert( ancestorNode instanceof Node );

    super( ancestorNode, providedOptions );

    this.setConfigToBaseDefault();
    this.mutateConfigurable( providedOptions );
    mutate( this, FLOW_CONSTRAINT_OPTION_KEYS, providedOptions );

    // Key configuration changes to relayout
    this.changedEmitter.addListener( this._updateLayoutListener );

    this.orientationChangedEmitter.addListener( () => this.cells.forEach( cell => {
      cell.orientation = this.orientation;
    } ) );
  }

  protected override layout(): void {
    super.layout();

    // The orientation along the laid-out lines
    const orientation = this._orientation;

    // The perpendicular orientation, where alignment is handled
    const oppositeOrientation = this._orientation.opposite;

    assert && assert( _.every( this.cells, cell => !cell.node.isDisposed ) );

    // Determine the first index of a visible non-divider (we need to NOT temporarily change visibility of dividers
    // back and forth, since this would trigger recursive layouts). We'll hide dividers until this.
    let firstVisibleNonDividerIndex = 0;
    for ( ; firstVisibleNonDividerIndex < this.cells.length; firstVisibleNonDividerIndex++ ) {
      const cell = this.cells[ firstVisibleNonDividerIndex ];
      if ( cell.node instanceof Divider ) {
        cell.node.visible = false;
      }
      else if ( cell.node.visible ) {
        break;
      }
    }

    // Scan for dividers, toggling visibility as desired. Leave the "last" divider visible, as if they are marking
    // sections "after" themselves.
    let hasVisibleNonDivider = false;
    for ( let i = this.cells.length - 1; i > firstVisibleNonDividerIndex; i-- ) {
      const cell = this.cells[ i ];
      if ( cell.node instanceof Divider ) {
        cell.node.visible = hasVisibleNonDivider;
        hasVisibleNonDivider = false;
      }
      else if ( cell.node.visible ) {
        hasVisibleNonDivider = true;
      }
    }

    const cells: FlowCell[] = this.cells.filter( cell => {
      return cell.isConnected() && cell.proxy.bounds.isValid() && ( !this.excludeInvisible || cell.node.visible );
    } );

    if ( !cells.length ) {
      this.layoutBoundsProperty.value = Bounds2.NOTHING;
      return;
    }

    // Determine our preferred sizes (they can be null, in which case)
    let preferredSize: number | null = this.getPreferredProperty( orientation ).value;
    const preferredOppositeSize: number | null = this.getPreferredProperty( oppositeOrientation ).value;

    // What is the largest of the minimum sizes of cells (e.g. if we're wrapping, this would be our minimum size)
    const maxMinimumCellSize: number = Math.max( ...cells.map( cell => cell.getMinimumSize( orientation ) || 0 ) );

    if ( maxMinimumCellSize > ( preferredSize || Number.POSITIVE_INFINITY ) ) {
      preferredSize = maxMinimumCellSize;
    }

    // Wrapping all the cells into lines
    const lines: FlowLine[] = [];
    if ( this.wrap ) {
      let currentLineCells: FlowCell[] = [];
      let availableSpace = preferredSize || Number.POSITIVE_INFINITY;

      while ( cells.length ) {
        const cell = cells.shift()!;
        const cellSpace = cell.getMinimumSize( orientation );

        if ( currentLineCells.length === 0 ) {
          currentLineCells.push( cell );
          availableSpace -= cellSpace;
        }
        else if ( this.spacing + cellSpace <= availableSpace + 1e-7 ) {
          currentLineCells.push( cell );
          availableSpace -= this.spacing + cellSpace;
        }
        else {
          lines.push( FlowLine.pool.create( orientation, currentLineCells ) );
          availableSpace = preferredSize || Number.POSITIVE_INFINITY;

          currentLineCells = [ cell ];
          availableSpace -= cellSpace;
        }
      }

      if ( currentLineCells.length ) {
        lines.push( FlowLine.pool.create( orientation, currentLineCells ) );
      }
    }
    else {
      lines.push( FlowLine.pool.create( orientation, cells ) );
    }

    lines.forEach( line => {
      line.cells.forEach( cell => {
        line.min = Math.max( line.min, cell.getMinimumSize( oppositeOrientation ) );
        line.max = Math.min( line.max, cell.getMaximumSize( oppositeOrientation ) );

        // For origin-specified cells, we will record their maximum reach from the origin, so these can be "summed"
        // (since the origin line may end up taking more space).
        if ( cell.effectiveAlign === LayoutAlign.ORIGIN ) {
          const originBounds = cell.getOriginBounds();
          line.minOrigin = Math.min( originBounds[ oppositeOrientation.minCoordinate ], line.minOrigin );
          line.maxOrigin = Math.max( originBounds[ oppositeOrientation.maxCoordinate ], line.maxOrigin );
        }
      } );

      if ( isFinite( line.minOrigin ) && isFinite( line.maxOrigin ) ) {
        line.size = Math.max( line.min, line.maxOrigin - line.minOrigin );
      }
      else {
        line.size = line.min;
      }
    } );

    // Given our wrapped lines, what is our minimum size we could take up?
    const minimumCurrentSize: number = Math.max( ...lines.map( line => line.getMinimumSize( this.spacing ) ) );
    const minimumCurrentOppositeSize = _.sum( lines.map( line => line.size ) ) + ( lines.length - 1 ) * this.lineSpacing;

    // Used for determining our "minimum" size for preferred sizes... if wrapping is enabled, we can be smaller than
    // current minimums
    const minimumAllowableSize = this.wrap ? maxMinimumCellSize : minimumCurrentSize;

    // Increase things if our preferred size is larger than our minimums (we'll figure out how to compensate
    // for the extra space below).
    const size = Math.max( minimumCurrentSize, preferredSize || 0 );
    const oppositeSize = Math.max( minimumCurrentOppositeSize, preferredOppositeSize || 0 );

    const originPrimary = this.layoutOriginProperty.value[ orientation.coordinate ];
    const originSecondary = this.layoutOriginProperty.value[ orientation.opposite.coordinate ];

    // Primary-direction layout
    lines.forEach( line => {
      const minimumContent = _.sum( line.cells.map( cell => cell.getMinimumSize( orientation ) ) );
      const spacingAmount = this.spacing * ( line.cells.length - 1 );
      let spaceRemaining = size - minimumContent - spacingAmount;

      // Initial pending sizes
      line.cells.forEach( cell => {
        cell._pendingSize = cell.getMinimumSize( orientation );
      } );

      // Grow potential sizes if possible
      let growableCells;
      while ( spaceRemaining > 1e-7 && ( growableCells = line.cells.filter( cell => {
        // Can the cell grow more?
        return cell.effectiveGrow !== 0 && cell._pendingSize < cell.getMaximumSize( orientation ) - 1e-7;
      } ) ).length ) {
        // Total sum of "grow" values in cells that could potentially grow
        const totalGrow = _.sum( growableCells.map( cell => cell.effectiveGrow ) );
        const amountToGrow = Math.min(
          // Smallest amount that any of the cells couldn't grow past (note: proportional to effectiveGrow)
          Math.min( ...growableCells.map( cell => ( cell.getMaximumSize( orientation ) - cell._pendingSize ) / cell.effectiveGrow ) ),

          // Amount each cell grows if all of our extra space fits in ALL the cells
          spaceRemaining / totalGrow
        );

        assert && assert( amountToGrow > 1e-11 );

        growableCells.forEach( cell => {
          cell._pendingSize += amountToGrow * cell.effectiveGrow!;
        } );
        spaceRemaining -= amountToGrow * totalGrow;
      }

      // Update preferred dimension based on the pending size
      line.cells.forEach( cell => cell.attemptPreferredSize( orientation, cell._pendingSize ) );

      const primarySpacingFunction = this._justify.spacingFunctionFactory( spaceRemaining, line.cells.length );

      let position = originPrimary;

      line.cells.forEach( ( cell, index ) => {
        position += primarySpacingFunction( index );
        if ( index > 0 ) {
          position += this.spacing;
        }
        cell.positionStart( orientation, position );
        position += cell._pendingSize;<commit_msg>Assertion for negative spacing larger than an element's size, see https://github.com/phetsims/sun/issues/48<commit_after>// Copyright 2021-2022, University of Colorado Boulder

/**
 * Main flow-layout logic. Usually used indirectly through FlowBox, but can also be used directly (say, if nodes don't
 * have the same parent, or a FlowBox can't be used).
 *
 * @author <NAME> <<EMAIL>>
 */

import Bounds2 from '../../../../dot/js/Bounds2.js';
import Orientation from '../../../../phet-core/js/Orientation.js';
import arrayRemove from '../../../../phet-core/js/arrayRemove.js';
import mutate from '../../../../phet-core/js/mutate.js';
import { Divider, ExternalFlowConfigurableOptions, FLOW_CONFIGURABLE_OPTION_KEYS, FlowCell, FlowConfigurable, FlowLine, HorizontalLayoutJustification, LayoutAlign, LayoutJustification, Node, NodeLayoutAvailableConstraintOptions, NodeLayoutConstraint, scenery, VerticalLayoutJustification } from '../../imports.js';
import IProperty from '../../../../axon/js/IProperty.js';

const FLOW_CONSTRAINT_OPTION_KEYS = [
  ...FLOW_CONFIGURABLE_OPTION_KEYS,
  'spacing',
  'lineSpacing',
  'justify',
  'justifyLines',
  'wrap',
  'excludeInvisible'
];

type SelfOptions = {
  // The default spacing in-between elements in the primary direction. If additional (or less) spacing is desired for
  // certain elements, per-element margins (even negative) can be set in the layoutOptions of nodes contained.
  spacing?: number;

  // The default spacing in-between lines in the secondary direction.
  lineSpacing?: number;

  // How extra space in the primary direction is allocated. The default is spaceBetween.
  justify?: HorizontalLayoutJustification | VerticalLayoutJustification;

  // How extra space in the secondary direction is allocated. The default is null (which will expand content to fit)
  justifyLines?: HorizontalLayoutJustification | VerticalLayoutJustification | null;

  // Whether line-wrapping is enabled. If so, the primary preferred dimension will determine where things are wrapped.
  wrap?: boolean;

  // The preferred width/height (ideally from a container's localPreferredWidth/localPreferredHeight.
  preferredWidthProperty?: IProperty<number | null>;
  preferredHeightProperty?: IProperty<number | null>;

  // The minimum width/height (ideally from a container's localMinimumWidth/localMinimumHeight.
  minimumWidthProperty?: IProperty<number | null>;
  minimumHeightProperty?: IProperty<number | null>;
};

export type FlowConstraintOptions = SelfOptions & ExternalFlowConfigurableOptions & NodeLayoutAvailableConstraintOptions;

export default class FlowConstraint extends FlowConfigurable( NodeLayoutConstraint ) {

  private readonly cells: FlowCell[] = [];
  private _justify: LayoutJustification = LayoutJustification.SPACE_BETWEEN;
  private _justifyLines: LayoutJustification | null = null;
  private _wrap = false;
  private _spacing = 0;
  private _lineSpacing = 0;

  constructor( ancestorNode: Node, providedOptions?: FlowConstraintOptions ) {
    assert && assert( ancestorNode instanceof Node );

    super( ancestorNode, providedOptions );

    this.setConfigToBaseDefault();
    this.mutateConfigurable( providedOptions );
    mutate( this, FLOW_CONSTRAINT_OPTION_KEYS, providedOptions );

    // Key configuration changes to relayout
    this.changedEmitter.addListener( this._updateLayoutListener );

    this.orientationChangedEmitter.addListener( () => this.cells.forEach( cell => {
      cell.orientation = this.orientation;
    } ) );
  }

  protected override layout(): void {
    super.layout();

    // The orientation along the laid-out lines
    const orientation = this._orientation;

    // The perpendicular orientation, where alignment is handled
    const oppositeOrientation = this._orientation.opposite;

    assert && assert( _.every( this.cells, cell => !cell.node.isDisposed ) );

    // Determine the first index of a visible non-divider (we need to NOT temporarily change visibility of dividers
    // back and forth, since this would trigger recursive layouts). We'll hide dividers until this.
    let firstVisibleNonDividerIndex = 0;
    for ( ; firstVisibleNonDividerIndex < this.cells.length; firstVisibleNonDividerIndex++ ) {
      const cell = this.cells[ firstVisibleNonDividerIndex ];
      if ( cell.node instanceof Divider ) {
        cell.node.visible = false;
      }
      else if ( cell.node.visible ) {
        break;
      }
    }

    // Scan for dividers, toggling visibility as desired. Leave the "last" divider visible, as if they are marking
    // sections "after" themselves.
    let hasVisibleNonDivider = false;
    for ( let i = this.cells.length - 1; i > firstVisibleNonDividerIndex; i-- ) {
      const cell = this.cells[ i ];
      if ( cell.node instanceof Divider ) {
        cell.node.visible = hasVisibleNonDivider;
        hasVisibleNonDivider = false;
      }
      else if ( cell.node.visible ) {
        hasVisibleNonDivider = true;
      }
    }

    const cells: FlowCell[] = this.cells.filter( cell => {
      return cell.isConnected() && cell.proxy.bounds.isValid() && ( !this.excludeInvisible || cell.node.visible );
    } );

    if ( !cells.length ) {
      this.layoutBoundsProperty.value = Bounds2.NOTHING;
      return;
    }

    // Determine our preferred sizes (they can be null, in which case)
    let preferredSize: number | null = this.getPreferredProperty( orientation ).value;
    const preferredOppositeSize: number | null = this.getPreferredProperty( oppositeOrientation ).value;

    // What is the largest of the minimum sizes of cells (e.g. if we're wrapping, this would be our minimum size)
    const maxMinimumCellSize: number = Math.max( ...cells.map( cell => cell.getMinimumSize( orientation ) || 0 ) );

    if ( maxMinimumCellSize > ( preferredSize || Number.POSITIVE_INFINITY ) ) {
      preferredSize = maxMinimumCellSize;
    }

    // Wrapping all the cells into lines
    const lines: FlowLine[] = [];
    if ( this.wrap ) {
      let currentLineCells: FlowCell[] = [];
      let availableSpace = preferredSize || Number.POSITIVE_INFINITY;

      while ( cells.length ) {
        const cell = cells.shift()!;
        const cellSpace = cell.getMinimumSize( orientation );

        if ( currentLineCells.length === 0 ) {
          currentLineCells.push( cell );
          availableSpace -= cellSpace;
        }
        else if ( this.spacing + cellSpace <= availableSpace + 1e-7 ) {
          currentLineCells.push( cell );
          availableSpace -= this.spacing + cellSpace;
        }
        else {
          lines.push( FlowLine.pool.create( orientation, currentLineCells ) );
          availableSpace = preferredSize || Number.POSITIVE_INFINITY;

          currentLineCells = [ cell ];
          availableSpace -= cellSpace;
        }
      }

      if ( currentLineCells.length ) {
        lines.push( FlowLine.pool.create( orientation, currentLineCells ) );
      }
    }
    else {
      lines.push( FlowLine.pool.create( orientation, cells ) );
    }

    lines.forEach( line => {
      line.cells.forEach( cell => {
        line.min = Math.max( line.min, cell.getMinimumSize( oppositeOrientation ) );
        line.max = Math.min( line.max, cell.getMaximumSize( oppositeOrientation ) );

        // For origin-specified cells, we will record their maximum reach from the origin, so these can be "summed"
        // (since the origin line may end up taking more space).
        if ( cell.effectiveAlign === LayoutAlign.ORIGIN ) {
          const originBounds = cell.getOriginBounds();
          line.minOrigin = Math.min( originBounds[ oppositeOrientation.minCoordinate ], line.minOrigin );
          line.maxOrigin = Math.max( originBounds[ oppositeOrientation.maxCoordinate ], line.maxOrigin );
        }
      } );

      if ( isFinite( line.minOrigin ) && isFinite( line.maxOrigin ) ) {
        line.size = Math.max( line.min, line.maxOrigin - line.minOrigin );
      }
      else {
        line.size = line.min;
      }
    } );

    // Given our wrapped lines, what is our minimum size we could take up?
    const minimumCurrentSize: number = Math.max( ...lines.map( line => line.getMinimumSize( this.spacing ) ) );
    const minimumCurrentOppositeSize = _.sum( lines.map( line => line.size ) ) + ( lines.length - 1 ) * this.lineSpacing;

    // Used for determining our "minimum" size for preferred sizes... if wrapping is enabled, we can be smaller than
    // current minimums
    const minimumAllowableSize = this.wrap ? maxMinimumCellSize : minimumCurrentSize;

    // Increase things if our preferred size is larger than our minimums (we'll figure out how to compensate
    // for the extra space below).
    const size = Math.max( minimumCurrentSize, preferredSize || 0 );
    const oppositeSize = Math.max( minimumCurrentOppositeSize, preferredOppositeSize || 0 );

    const originPrimary = this.layoutOriginProperty.value[ orientation.coordinate ];
    const originSecondary = this.layoutOriginProperty.value[ orientation.opposite.coordinate ];

    // Primary-direction layout
    lines.forEach( line => {
      const minimumContent = _.sum( line.cells.map( cell => cell.getMinimumSize( orientation ) ) );
      const spacingAmount = this.spacing * ( line.cells.length - 1 );
      let spaceRemaining = size - minimumContent - spacingAmount;

      // Initial pending sizes
      line.cells.forEach( cell => {
        cell._pendingSize = cell.getMinimumSize( orientation );
      } );

      // Grow potential sizes if possible
      let growableCells;
      while ( spaceRemaining > 1e-7 && ( growableCells = line.cells.filter( cell => {
        // Can the cell grow more?
        return cell.effectiveGrow !== 0 && cell._pendingSize < cell.getMaximumSize( orientation ) - 1e-7;
      } ) ).length ) {
        // Total sum of "grow" values in cells that could potentially grow
        const totalGrow = _.sum( growableCells.map( cell => cell.effectiveGrow ) );
        const amountToGrow = Math.min(
          // Smallest amount that any of the cells couldn't grow past (note: proportional to effectiveGrow)
          Math.min( ...growableCells.map( cell => ( cell.getMaximumSize( orientation ) - cell._pendingSize ) / cell.effectiveGrow ) ),

          // Amount each cell grows if all of our extra space fits in ALL the cells
          spaceRemaining / totalGrow
        );

        assert && assert( amountToGrow > 1e-11 );

        growableCells.forEach( cell => {
          cell._pendingSize += amountToGrow * cell.effectiveGrow!;
        } );
        spaceRemaining -= amountToGrow * totalGrow;
      }

      // Update preferred dimension based on the pending size
      line.cells.forEach( cell => cell.attemptPreferredSize( orientation, cell._pendingSize ) );

      const primarySpacingFunction = this._justify.spacingFunctionFactory( spaceRemaining, line.cells.length );

      let position = originPrimary;

      line.cells.forEach( ( cell, index ) => {
        position += primarySpacingFunction( index );
        if ( index > 0 ) {
          position += this.spacing;
        }
        cell.positionStart( orientation, position );
        position += cell._pendingSize;
        assert && assert( this.spacing >= 0 || cell._pendingSize >= -this.spacing,
          'Negative spacing more than a cell\'s size causes issues with layout' );
{'fraction_non_alphanumeric': 0.1041220556745182, 'fraction_numerical': 0.0029889364739471807, 'mean_word_length': 3.045659628225952, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 65, 'https://': 1, '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': '4138481', 'n_tokens_mistral': 6243, 'n_tokens_neox': 5884, 'n_words': 1796}