content: /*
   * Copyright (C) 2010 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.graphics.text;
  
  import android.annotation.FloatRange;
  import android.annotation.IntRange;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.Px;
  import android.graphics.Paint;
  import android.graphics.Rect;
  
  import com.android.internal.util.Preconditions;
  
  import dalvik.annotation.optimization.CriticalNative;
  
  import libcore.util.NativeAllocationRegistry;
  
  /**
   * Result of text shaping of the single paragraph string.
   *
   * <p>
   * <pre>
   * <code>
   * Paint paint = new Paint();
   * Paint bigPaint = new Paint();
   * bigPaint.setTextSize(paint.getTextSize() * 2.0);
   * String text = "Hello, Android.";
   * MeasuredText mt = new MeasuredText.Builder(text.toCharArray())
   *      .appendStyleRun(paint, 7, false)  // Use paint for "Hello, "
   *      .appendStyleRun(bigPaint, 8, false)  // Use bigPaint for "Android."
   *      .build();
   * </code>
   * </pre>
   * </p>
   */
  public class MeasuredText {
      private long mNativePtr;
      private boolean mComputeHyphenation;
      private boolean mComputeLayout;
      private @NonNull char[] mChars;
  
      // Use builder instead.
      private MeasuredText(long ptr, @NonNull char[] chars, boolean computeHyphenation,
              boolean computeLayout) {
          mNativePtr = ptr;
          mChars = chars;
          mComputeHyphenation = computeHyphenation;
          mComputeLayout = computeLayout;
      }
  
      /**
       * Returns the characters in the paragraph used to compute this MeasuredText instance.
       * @hide
       */
      public @NonNull char[] getChars() {
          return mChars;
      }
  
      /**
       * Returns the width of a given range.
       *
       * @param start an inclusive start index of the range
       * @param end an exclusive end index of the range
       */
      public @FloatRange(from = 0.0) @Px float getWidth(
              @IntRange(from = 0) int start, @IntRange(from = 0) int end) {
          Preconditions.checkArgument(0 <= start && start <= mChars.length,
                  "start(" + start + ") must be 0 <= start <= " + mChars.length);
          Preconditions.checkArgument(0 <= end && end <= mChars.length,
                  "end(" + end + ") must be 0 <= end <= " + mChars.length);
          Preconditions.checkArgument(start <= end,
                  "start(" + start + ") is larger than end(" + end + ")");
          return nGetWidth(mNativePtr, start, end);
      }
  
      /**
       * Returns a memory usage of the native object.
       *
       * @hide
       */
      public int getMemoryUsage() {
          return nGetMemoryUsage(mNativePtr);
      }
  
      /**
       * Retrieves the boundary box of the given range
       *
       * @param start an inclusive start index of the range
       * @param end an exclusive end index of the range
       * @param rect an output parameter
       */
      public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
              @NonNull Rect rect) {
          Preconditions.checkArgument(0 <= start && start <= mChars.length,
                  "start(" + start + ") must be 0 <= start <= " + mChars.length);
          Preconditions.checkArgument(0 <= end && end <= mChars.length,
                  "end(" + end + ") must be 0 <= end <= " + mChars.length);
          Preconditions.checkArgument(start <= end,
                  "start(" + start + ") is larger than end(" + end + ")");
          Preconditions.checkNotNull(rect);
          nGetBounds(mNativePtr, mChars, start, end, rect);
      }
  
      /**
       * Returns the width of the character at the given offset.
       *
       * @param offset an offset of the character.
       */
      public @FloatRange(from = 0.0f) @Px float getCharWidthAt(@IntRange(from = 0) int offset) {
          Preconditions.checkArgument(0 <= offset && offset < mChars.length,
                  "offset(" + offset + ") is larger than text length: " + mChars.length);
          return nGetCharWidthAt(mNativePtr, offset);
      }
  
      /**
       * Returns a native pointer of the underlying native object.
       *
       * @hide
       */
      public long getNativePtr() {
          return mNativePtr;
      }
  
      @CriticalNative
      private static native float nGetWidth(/* Non Zero */ long nativePtr,
                                           @IntRange(from = 0) int start,
                                           @IntRange(from = 0) int end);
  
      @CriticalNative
      private static native /* Non Zero */ long nGetReleaseFunc();
  
      @CriticalNative
      private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr);
  
      private static native void nGetBounds(long nativePtr, char[] buf, int start, int end,
              Rect rect);
  
      @CriticalNative
      private static native float nGetCharWidthAt(long nativePtr, int offset);
  
      /**
       * Helper class for creating a {@link MeasuredText}.
       * <p>
       * <pre>
       * <code>
       * Paint paint = new Paint();
       * String text = "Hello, Android.";
       * MeasuredText mt = new MeasuredText.Builder(text.toCharArray())
       *      .appendStyleRun(paint, text.length, false)
       *      .build();
       * </code>
       * </pre>
       * </p>
       *
       * Note: The appendStyle and appendReplacementRun should be called to cover the text length.
       */
      public static final class Builder {
          private static final NativeAllocationRegistry sRegistry =
                  NativeAllocationRegistry.createMalloced(
                  MeasuredText.class.getClassLoader(), nGetReleaseFunc());
  
          private long mNativePtr;
  
          private final @NonNull char[] mText;
          private boolean mComputeHyphenation = false;
          private boolean mComputeLayout = true;
          private int mCurrentOffset = 0;
          private @Nullable MeasuredText mHintMt = null;
  
          /**
           * Construct a builder.
           *
           * The MeasuredText returned by build method will hold a reference of the text. Developer is
           * not supposed to modify the text.
           *
           * @param text a text
           */
          public Builder(@NonNull char[] text) {
              Preconditions.checkNotNull(text);
              mText = text;
              mNativePtr = nInitBuilder();
          }
  
          /**
           * Construct a builder with existing MeasuredText.
           *
           * The MeasuredText returned by build method will hold a reference of the text. Developer is
           * not supposed to modify the text.
           *
           * @param text a text
           */
          public Builder(@NonNull MeasuredText text) {
              Preconditions.checkNotNull(text);
              mText = text.mChars;
              mNativePtr = nInitBuilder();
              if (!text.mComputeLayout) {
                  throw new IllegalArgumentException(
                      "The input MeasuredText must not be created with setComputeLayout(false).");
              }
              mComputeHyphenation = text.mComputeHyphenation;
              mComputeLayout = text.mComputeLayout;
              mHintMt = text;
          }
  
          /**
           * Apply styles to the given length.
           *
           * Keeps an internal offset which increases at every append. The initial value for this
           * offset is zero. After the style is applied the internal offset is moved to {@code offset
           * + length}, and next call will start from this new position.
           *
           * @param paint a paint
           * @param length a length to be applied with a given paint, can not exceed the length of the
           *               text
           * @param isRtl true if the text is in RTL context, otherwise false.
           */
          public @NonNull Builder appendStyleRun(@NonNull Paint paint, @IntRange(from = 0) int length,
                  boolean isRtl) {
              Preconditions.checkNotNull(paint);
              Preconditions.checkArgument(length > 0, "length can not be negative");
              final int end = mCurrentOffset + length;
              Preconditions.checkArgument(end <= mText.length, "Style exceeds the text length");
              nAddStyleRun(mNativePtr, paint.getNativeInstance(), mCurrentOffset, end, isRtl);
              mCurrentOffset = end;
              return this;
          }
  
          /**
           * Used to inform the text layout that the given length is replaced with the object of given
           * width.
           *
           * Keeps an internal offset which increases at every append. The initial value for this
           * offset is zero. After the style is applied the internal offset is moved to {@code offset
           * + length}, and next call will start from this new position.
           *
           * Informs the layout engine that the given length should not be processed, instead the
           * provided width should be used for calculating the width of that range.
           *
           * @param length a length to be replaced with the object, can not exceed the length of the
           *               text
           * @param width a replacement width of the range
           */
          public @NonNull Builder appendReplacementRun(@NonNull Paint paint,
                  @IntRange(from = 0) int length, @Px @FloatRange(from = 0) float width) {
              Preconditions.checkArgument(length > 0, "length can not be negative");
              final int end = mCurrentOffset + length;
              Preconditions.checkArgument(end <= mText.length, "Replacement exceeds the text length");
              nAddReplacementRun(mNativePtr, paint.getNativeInstance(), mCurrentOffset, end, width);
              mCurrentOffset = end;
              return this;
          }
  
          /**
           * By passing true to this method, the build method will compute all possible hyphenation
           * pieces as well.
           *
           * If you don't want to use automatic hyphenation, you can pass false to this method and
           * save the computation time of hyphenation. The default value is false.
           *
           * Even if you pass false to this method, you can still enable automatic hyphenation of
           * LineBreaker but line break computation becomes slower.
           *
           * @param computeHyphenation true if you want to use automatic hyphenations.
           */
          public @NonNull Builder setComputeHyphenation(boolean computeHyphenation) {
              mComputeHyphenation = computeHyphenation;
              return this;
          }
  
          /**
           * By passing true to this method, the build method will compute all full layout
           * information.
           *
           * If you don't use {@link MeasuredText#getBounds(int,int,android.graphics.Rect)}, you can
           * pass false to this method and save the memory spaces. The default value is true.
           *
           * Even if you pass false to this method, you can still call getBounds but it becomes
           * slower.
           *
           * @param computeLayout true if you want to retrieve full layout info, e.g. bbox.
           */
          public @NonNull Builder setComputeLayout(boolean computeLayout) {
              mComputeLayout = computeLayout;
              return this;
          }
  
          /**
           * Creates a MeasuredText.
           *
           * Once you called build() method, you can't reuse the Builder class again.
           * @throws IllegalStateException if this Builder is reused.
           * @throws IllegalStateException if the whole text is not covered by one or more runs (style
           *                               or replacement)
           */
          public @NonNull MeasuredText build() {
              ensureNativePtrNoReuse();
              if (mCurrentOffset != mText.length) {
                  throw new IllegalStateException("Style info has not been provided for all text.");
              }
              if (mHintMt != null && mHintMt.mComputeHyphenation != mComputeHyphenation) {
                  throw new IllegalArgumentException(
                          "The hyphenation configuration is different from given hint MeasuredText");
              }
              try {
                  long hintPtr = (mHintMt == null) ? 0 : mHintMt.getNativePtr();
                  long ptr = nBuildMeasuredText(mNativePtr, hintPtr, mText, mComputeHyphenation,
                          mComputeLayout);
                  final MeasuredText res = new MeasuredText(ptr, mText, mComputeHyphenation,
                          mComputeLayout);
                  sRegistry.registerNativeAllocation(res, ptr);
                  return res;
              } finally {
                  nFreeBuilder(mNativePtr);
                  mNativePtr = 0;
              }
          }
  
          /**
           * Ensures {@link #mNativePtr} is not reused.
           *
           * <p/> This is a method by itself to help increase testability - eg. Robolectric might want
           * to override the validation behavior in test environment.
           */
          private void ensureNativePtrNoReuse() {
              if (mNativePtr == 0) {
                  throw new IllegalStateException("Builder can not be reused.");
              }
          }
  
          private static native /* Non Zero */ long nInitBuilder();
  
          /**
           * Apply style to make native measured text.
           *
           * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
           * @param paintPtr The native paint pointer to be applied.
           * @param start The start offset in the copied buffer.
           * @param end The end offset in the copied buffer.
           * @param isRtl True if the text is RTL.
           */
          private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr,
                                                  /* Non Zero */ long paintPtr,
                                                  @IntRange(from = 0) int start,
                                                  @IntRange(from = 0) int end,
                                                  boolean isRtl);
          /**
           * Apply ReplacementRun to make native measured text.
           *
           * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
           * @param paintPtr The native paint pointer to be applied.
           * @param start The start offset in the copied buffer.
           * @param end The end offset in the copied buffer.
           * @param width The width of the replacement.
           */
          private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr,
                                                        /* Non Zero */ long paintPtr,
                                                        @IntRange(from = 0) int start,
                                                        @IntRange(from = 0) int end,
                                                        @FloatRange(from = 0) float width);
  
          private static native long nBuildMeasuredText(
                  /* Non Zero */ long nativeBuilderPtr,
                  long hintMtPtr,
                  @NonNull char[] text,
                  boolean computeHyphenation,
                  boolean computeLayout);
  
          private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
      }
  }
  