Blob Blame History Raw
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=4 sw=4 sts=4 et cindent: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsTextBoxFrame.h"

#include "gfx2DGlue.h"
#include "gfxUtils.h"
#include "mozilla/gfx/2D.h"
#include "nsFontMetrics.h"
#include "nsReadableUtils.h"
#include "nsCOMPtr.h"
#include "nsGkAtoms.h"
#include "nsPresContext.h"
#include "gfxContext.h"
#include "nsStyleContext.h"
#include "nsIContent.h"
#include "nsNameSpaceManager.h"
#include "nsBoxLayoutState.h"
#include "nsMenuBarListener.h"
#include "nsString.h"
#include "nsIServiceManager.h"
#include "nsIDOMElement.h"
#include "nsIDOMXULLabelElement.h"
#include "mozilla/EventStateManager.h"
#include "nsITheme.h"
#include "nsUnicharUtils.h"
#include "nsContentUtils.h"
#include "nsDisplayList.h"
#include "nsCSSRendering.h"
#include "nsIReflowCallback.h"
#include "nsBoxFrame.h"
#include "mozilla/Preferences.h"
#include "nsLayoutUtils.h"
#include "mozilla/Attributes.h"
#include "nsUnicodeProperties.h"

#ifdef ACCESSIBILITY
#include "nsAccessibilityService.h"
#endif

#include "nsBidiUtils.h"
#include "nsBidiPresUtils.h"

using namespace mozilla;
using namespace mozilla::gfx;

class nsAccessKeyInfo {
 public:
  int32_t mAccesskeyIndex;
  nscoord mBeforeWidth, mAccessWidth, mAccessUnderlineSize, mAccessOffset;
};

bool nsTextBoxFrame::gAlwaysAppendAccessKey = false;
bool nsTextBoxFrame::gAccessKeyPrefInitialized = false;
bool nsTextBoxFrame::gInsertSeparatorBeforeAccessKey = false;
bool nsTextBoxFrame::gInsertSeparatorPrefInitialized = false;

nsIFrame* NS_NewTextBoxFrame(nsIPresShell* aPresShell,
                             nsStyleContext* aContext) {
  return new (aPresShell) nsTextBoxFrame(aContext);
}

NS_IMPL_FRAMEARENA_HELPERS(nsTextBoxFrame)

NS_QUERYFRAME_HEAD(nsTextBoxFrame)
NS_QUERYFRAME_ENTRY(nsTextBoxFrame)
NS_QUERYFRAME_TAIL_INHERITING(nsLeafBoxFrame)

nsresult nsTextBoxFrame::AttributeChanged(int32_t aNameSpaceID,
                                          nsAtom* aAttribute,
                                          int32_t aModType) {
  bool aResize;
  bool aRedraw;

  UpdateAttributes(aAttribute, aResize, aRedraw);

  if (aResize) {
    PresShell()->FrameNeedsReflow(this, nsIPresShell::eStyleChange,
                                  NS_FRAME_IS_DIRTY);
  } else if (aRedraw) {
    nsBoxLayoutState state(PresContext());
    XULRedraw(state);
  }

  // If the accesskey changed, register for the new value
  // The old value has been unregistered in nsXULElement::SetAttr
  if (aAttribute == nsGkAtoms::accesskey || aAttribute == nsGkAtoms::control)
    RegUnregAccessKey(true);

  return NS_OK;
}

nsTextBoxFrame::nsTextBoxFrame(nsStyleContext* aContext)
    : nsLeafBoxFrame(aContext, kClassID),
      mAccessKeyInfo(nullptr),
      mCropType(CropRight),
      mAscent(0),
      mNeedsReflowCallback(false) {
  MarkIntrinsicISizesDirty();
}

nsTextBoxFrame::~nsTextBoxFrame() { delete mAccessKeyInfo; }

void nsTextBoxFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
                          nsIFrame* aPrevInFlow) {
  nsLeafBoxFrame::Init(aContent, aParent, aPrevInFlow);

  bool aResize;
  bool aRedraw;
  UpdateAttributes(nullptr, aResize, aRedraw); /* update all */

  // register access key
  RegUnregAccessKey(true);
}

void nsTextBoxFrame::DestroyFrom(nsIFrame* aDestructRoot,
                                 PostDestroyData& aPostDestroyData) {
  // unregister access key
  RegUnregAccessKey(false);
  nsLeafBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
}

bool nsTextBoxFrame::AlwaysAppendAccessKey() {
  if (!gAccessKeyPrefInitialized) {
    gAccessKeyPrefInitialized = true;

    const char* prefName = "intl.menuitems.alwaysappendaccesskeys";
    nsAutoString val;
    Preferences::GetLocalizedString(prefName, val);
    gAlwaysAppendAccessKey = val.EqualsLiteral("true");
  }
  return gAlwaysAppendAccessKey;
}

bool nsTextBoxFrame::InsertSeparatorBeforeAccessKey() {
  if (!gInsertSeparatorPrefInitialized) {
    gInsertSeparatorPrefInitialized = true;

    const char* prefName = "intl.menuitems.insertseparatorbeforeaccesskeys";
    nsAutoString val;
    Preferences::GetLocalizedString(prefName, val);
    gInsertSeparatorBeforeAccessKey = val.EqualsLiteral("true");
  }
  return gInsertSeparatorBeforeAccessKey;
}

class nsAsyncAccesskeyUpdate final : public nsIReflowCallback {
 public:
  explicit nsAsyncAccesskeyUpdate(nsIFrame* aFrame) : mWeakFrame(aFrame) {}

  virtual bool ReflowFinished() override {
    bool shouldFlush = false;
    nsTextBoxFrame* frame = static_cast<nsTextBoxFrame*>(mWeakFrame.GetFrame());
    if (frame) {
      shouldFlush = frame->UpdateAccesskey(mWeakFrame);
    }
    delete this;
    return shouldFlush;
  }

  virtual void ReflowCallbackCanceled() override { delete this; }

  WeakFrame mWeakFrame;
};

bool nsTextBoxFrame::UpdateAccesskey(WeakFrame& aWeakThis) {
  nsAutoString accesskey;
  nsCOMPtr<nsIDOMXULLabelElement> labelElement = do_QueryInterface(mContent);
  NS_ENSURE_TRUE(aWeakThis.IsAlive(), false);
  if (labelElement) {
    // Accesskey may be stored on control.
    labelElement->GetAccessKey(accesskey);
    NS_ENSURE_TRUE(aWeakThis.IsAlive(), false);
  } else {
    mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
                                   accesskey);
  }

  if (!accesskey.Equals(mAccessKey)) {
    // Need to get clean mTitle.
    RecomputeTitle();
    mAccessKey = accesskey;
    UpdateAccessTitle();
    PresShell()->FrameNeedsReflow(this, nsIPresShell::eStyleChange,
                                  NS_FRAME_IS_DIRTY);
    return true;
  }
  return false;
}

void nsTextBoxFrame::UpdateAttributes(nsAtom* aAttribute, bool& aResize,
                                      bool& aRedraw) {
  bool doUpdateTitle = false;
  aResize = false;
  aRedraw = false;

  if (aAttribute == nullptr || aAttribute == nsGkAtoms::crop) {
    static Element::AttrValuesArray strings[] = {&nsGkAtoms::left,
                                                 &nsGkAtoms::start,
                                                 &nsGkAtoms::center,
                                                 &nsGkAtoms::right,
                                                 &nsGkAtoms::end,
                                                 &nsGkAtoms::none,
                                                 nullptr};
    CroppingStyle cropType;
    switch (mContent->AsElement()->FindAttrValueIn(
        kNameSpaceID_None, nsGkAtoms::crop, strings, eCaseMatters)) {
      case 0:
      case 1:
        cropType = CropLeft;
        break;
      case 2:
        cropType = CropCenter;
        break;
      case 3:
      case 4:
        cropType = CropRight;
        break;
      case 5:
        cropType = CropNone;
        break;
      default:
        cropType = CropAuto;
        break;
    }

    if (cropType != mCropType) {
      aResize = true;
      mCropType = cropType;
    }
  }

  if (aAttribute == nullptr || aAttribute == nsGkAtoms::value) {
    RecomputeTitle();
    doUpdateTitle = true;
  }

  if (aAttribute == nullptr || aAttribute == nsGkAtoms::accesskey) {
    mNeedsReflowCallback = true;
    // Ensure that layout is refreshed and reflow callback called.
    aResize = true;
  }

  if (doUpdateTitle) {
    UpdateAccessTitle();
    aResize = true;
  }
}

class nsDisplayXULTextBox final : public nsDisplayItem {
 public:
  nsDisplayXULTextBox(nsDisplayListBuilder* aBuilder, nsTextBoxFrame* aFrame)
      : nsDisplayItem(aBuilder, aFrame) {
    MOZ_COUNT_CTOR(nsDisplayXULTextBox);
  }
#ifdef NS_BUILD_REFCNT_LOGGING
  virtual ~nsDisplayXULTextBox() { MOZ_COUNT_DTOR(nsDisplayXULTextBox); }
#endif

  virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
  virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder,
                           bool* aSnap) const override;
  NS_DISPLAY_DECL_NAME("XULTextBox", TYPE_XUL_TEXT_BOX)

  virtual nsRect GetComponentAlphaBounds(
      nsDisplayListBuilder* aBuilder) const override;

  void PaintTextToContext(gfxContext* aCtx, nsPoint aOffset,
                          const nscolor* aColor);
};

static void PaintTextShadowCallback(gfxContext* aCtx, nsPoint aShadowOffset,
                                    const nscolor& aShadowColor, void* aData) {
  reinterpret_cast<nsDisplayXULTextBox*>(aData)->PaintTextToContext(
      aCtx, aShadowOffset, &aShadowColor);
}

void nsDisplayXULTextBox::Paint(nsDisplayListBuilder* aBuilder,
                                gfxContext* aCtx) {
  DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
                                                    mDisableSubpixelAA);

  // Paint the text shadow before doing any foreground stuff
  nsRect drawRect =
      static_cast<nsTextBoxFrame*>(mFrame)->mTextDrawRect + ToReferenceFrame();
  nsLayoutUtils::PaintTextShadow(mFrame, aCtx, drawRect, mVisibleRect,
                                 mFrame->StyleColor()->mColor,
                                 PaintTextShadowCallback, (void*)this);

  PaintTextToContext(aCtx, nsPoint(0, 0), nullptr);
}

void nsDisplayXULTextBox::PaintTextToContext(gfxContext* aCtx, nsPoint aOffset,
                                             const nscolor* aColor) {
  static_cast<nsTextBoxFrame*>(mFrame)->PaintTitle(
      *aCtx, mVisibleRect, ToReferenceFrame() + aOffset, aColor);
}

nsRect nsDisplayXULTextBox::GetBounds(nsDisplayListBuilder* aBuilder,
                                      bool* aSnap) const {
  *aSnap = false;
  return mFrame->GetVisualOverflowRectRelativeToSelf() + ToReferenceFrame();
}

nsRect nsDisplayXULTextBox::GetComponentAlphaBounds(
    nsDisplayListBuilder* aBuilder) const {
  return static_cast<nsTextBoxFrame*>(mFrame)->GetComponentAlphaBounds() +
         ToReferenceFrame();
}

void nsTextBoxFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
                                      const nsDisplayListSet& aLists) {
  if (!IsVisibleForPainting(aBuilder)) return;

  nsLeafBoxFrame::BuildDisplayList(aBuilder, aLists);

  aLists.Content()->AppendToTop(
      MakeDisplayItem<nsDisplayXULTextBox>(aBuilder, this));
}

void nsTextBoxFrame::PaintTitle(gfxContext& aRenderingContext,
                                const nsRect& aDirtyRect, nsPoint aPt,
                                const nscolor* aOverrideColor) {
  if (mTitle.IsEmpty()) return;

  DrawText(aRenderingContext, aDirtyRect, mTextDrawRect + aPt, aOverrideColor);
}

void nsTextBoxFrame::DrawText(gfxContext& aRenderingContext,
                              const nsRect& aDirtyRect, const nsRect& aTextRect,
                              const nscolor* aOverrideColor) {
  nsPresContext* presContext = PresContext();
  int32_t appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel();
  DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();

  // paint the title
  nscolor overColor = 0;
  nscolor underColor = 0;
  nscolor strikeColor = 0;
  uint8_t overStyle = 0;
  uint8_t underStyle = 0;
  uint8_t strikeStyle = 0;

  // Begin with no decorations
  uint8_t decorations = NS_STYLE_TEXT_DECORATION_LINE_NONE;
  // A mask of all possible decorations.
  uint8_t decorMask = NS_STYLE_TEXT_DECORATION_LINE_LINES_MASK;

  WritingMode wm = GetWritingMode();
  bool vertical = wm.IsVertical();

  nsIFrame* f = this;
  do {  // find decoration colors
    nsStyleContext* context = f->StyleContext();
    if (!context->HasTextDecorationLines()) {
      break;
    }
    const nsStyleTextReset* styleText = context->StyleTextReset();

    if (decorMask &
        styleText->mTextDecorationLine) {  // a decoration defined here
      nscolor color;
      if (aOverrideColor) {
        color = *aOverrideColor;
      } else {
        color = context->StyleColor()->CalcComplexColor(
            styleText->mTextDecorationColor);
      }
      uint8_t style = styleText->mTextDecorationStyle;

      if (NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE & decorMask &
          styleText->mTextDecorationLine) {
        underColor = color;
        underStyle = style;
        decorMask &= ~NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE;
        decorations |= NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE;
      }
      if (NS_STYLE_TEXT_DECORATION_LINE_OVERLINE & decorMask &
          styleText->mTextDecorationLine) {
        overColor = color;
        overStyle = style;
        decorMask &= ~NS_STYLE_TEXT_DECORATION_LINE_OVERLINE;
        decorations |= NS_STYLE_TEXT_DECORATION_LINE_OVERLINE;
      }
      if (NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH & decorMask &
          styleText->mTextDecorationLine) {
        strikeColor = color;
        strikeStyle = style;
        decorMask &= ~NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH;
        decorations |= NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH;
      }
    }
  } while (0 != decorMask && (f = nsLayoutUtils::GetParentOrPlaceholderFor(f)));

  RefPtr<nsFontMetrics> fontMet =
      nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
  fontMet->SetVertical(wm.IsVertical());
  fontMet->SetTextOrientation(StyleVisibility()->mTextOrientation);

  nscoord offset;
  nscoord size;
  nscoord ascent = fontMet->MaxAscent();

  nsPoint baselinePt;
  if (wm.IsVertical()) {
    baselinePt.x = presContext->RoundAppUnitsToNearestDevPixels(
        aTextRect.x + (wm.IsVerticalRL() ? aTextRect.width - ascent : ascent));
    baselinePt.y = aTextRect.y;
  } else {
    baselinePt.x = aTextRect.x;
    baselinePt.y =
        presContext->RoundAppUnitsToNearestDevPixels(aTextRect.y + ascent);
  }

  nsCSSRendering::PaintDecorationLineParams params;
  params.dirtyRect = ToRect(presContext->AppUnitsToGfxUnits(aDirtyRect));
  params.pt = Point(presContext->AppUnitsToGfxUnits(aTextRect.x),
                    presContext->AppUnitsToGfxUnits(aTextRect.y));
  params.icoordInFrame =
      Float(PresContext()->AppUnitsToGfxUnits(mTextDrawRect.x));
  params.lineSize = Size(presContext->AppUnitsToGfxUnits(aTextRect.width), 0);
  params.ascent = presContext->AppUnitsToGfxUnits(ascent);
  params.vertical = vertical;

  // XXX todo: vertical-mode support for decorations not tested yet,
  // probably won't be positioned correctly

  // Underlines are drawn before overlines, and both before the text
  // itself, per http://www.w3.org/TR/CSS21/zindex.html point 7.2.1.4.1.1.
  // (We don't apply this rule to the access-key underline because we only
  // find out where that is as a side effect of drawing the text, in the
  // general case -- see below.)
  if (decorations & (NS_STYLE_TEXT_DECORATION_LINE_OVERLINE |
                     NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE)) {
    fontMet->GetUnderline(offset, size);
    params.lineSize.height = presContext->AppUnitsToGfxUnits(size);
    if ((decorations & NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE) &&
        underStyle != NS_STYLE_TEXT_DECORATION_STYLE_NONE) {
      params.color = underColor;
      params.offset = presContext->AppUnitsToGfxUnits(offset);
      params.decoration = NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE;
      params.style = underStyle;
      nsCSSRendering::PaintDecorationLine(this, *drawTarget, params);
    }
    if ((decorations & NS_STYLE_TEXT_DECORATION_LINE_OVERLINE) &&
        overStyle != NS_STYLE_TEXT_DECORATION_STYLE_NONE) {
      params.color = overColor;
      params.offset = params.ascent;
      params.decoration = NS_STYLE_TEXT_DECORATION_LINE_OVERLINE;
      params.style = overStyle;
      nsCSSRendering::PaintDecorationLine(this, *drawTarget, params);
    }
  }

  RefPtr<gfxContext> refContext =
      PresShell()->CreateReferenceRenderingContext();
  DrawTarget* refDrawTarget = refContext->GetDrawTarget();

  CalculateUnderline(refDrawTarget, *fontMet);

  nscolor c = aOverrideColor ? *aOverrideColor : StyleColor()->mColor;
  ColorPattern color(ToDeviceColor(c));
  aRenderingContext.SetColor(Color::FromABGR(c));

  nsresult rv = NS_ERROR_FAILURE;

  if (mState & NS_FRAME_IS_BIDI) {
    presContext->SetBidiEnabled();
    nsBidiLevel level = nsBidiPresUtils::BidiLevelFromStyle(StyleContext());
    if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
      // We let the RenderText function calculate the mnemonic's
      // underline position for us.
      nsBidiPositionResolve posResolve;
      posResolve.logicalIndex = mAccessKeyInfo->mAccesskeyIndex;
      rv = nsBidiPresUtils::RenderText(
          mCroppedTitle.get(), mCroppedTitle.Length(), level, presContext,
          aRenderingContext, refDrawTarget, *fontMet, baselinePt.x,
          baselinePt.y, &posResolve, 1);
      mAccessKeyInfo->mBeforeWidth = posResolve.visualLeftTwips;
      mAccessKeyInfo->mAccessWidth = posResolve.visualWidth;
    } else {
      rv = nsBidiPresUtils::RenderText(
          mCroppedTitle.get(), mCroppedTitle.Length(), level, presContext,
          aRenderingContext, refDrawTarget, *fontMet, baselinePt.x,
          baselinePt.y);
    }
  }
  if (NS_FAILED(rv)) {
    fontMet->SetTextRunRTL(false);

    if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
      // In the simple (non-BiDi) case, we calculate the mnemonic's
      // underline position by getting the text metric.
      // XXX are attribute values always two byte?
      if (mAccessKeyInfo->mAccesskeyIndex > 0)
        mAccessKeyInfo->mBeforeWidth = nsLayoutUtils::AppUnitWidthOfString(
            mCroppedTitle.get(), mAccessKeyInfo->mAccesskeyIndex, *fontMet,
            refDrawTarget);
      else
        mAccessKeyInfo->mBeforeWidth = 0;
    }

    fontMet->DrawString(mCroppedTitle.get(), mCroppedTitle.Length(),
                        baselinePt.x, baselinePt.y, &aRenderingContext,
                        refDrawTarget);
  }

  if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
    nsRect r(aTextRect.x + mAccessKeyInfo->mBeforeWidth,
             aTextRect.y + mAccessKeyInfo->mAccessOffset,
             mAccessKeyInfo->mAccessWidth,
             mAccessKeyInfo->mAccessUnderlineSize);
    Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget);
    drawTarget->FillRect(devPxRect, color);
  }

  // Strikeout is drawn on top of the text, per
  // http://www.w3.org/TR/CSS21/zindex.html point 7.2.1.4.1.1.
  if ((decorations & NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH) &&
      strikeStyle != NS_STYLE_TEXT_DECORATION_STYLE_NONE) {
    fontMet->GetStrikeout(offset, size);
    params.color = strikeColor;
    params.lineSize.height = presContext->AppUnitsToGfxUnits(size);
    params.offset = presContext->AppUnitsToGfxUnits(offset);
    params.decoration = NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH;
    params.style = strikeStyle;
    nsCSSRendering::PaintDecorationLine(this, *drawTarget, params);
  }
}

void nsTextBoxFrame::CalculateUnderline(DrawTarget* aDrawTarget,
                                        nsFontMetrics& aFontMetrics) {
  if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
    // Calculate all fields of mAccessKeyInfo which
    // are the same for both BiDi and non-BiDi frames.
    const char16_t* titleString = mCroppedTitle.get();
    aFontMetrics.SetTextRunRTL(false);
    mAccessKeyInfo->mAccessWidth = nsLayoutUtils::AppUnitWidthOfString(
        titleString[mAccessKeyInfo->mAccesskeyIndex], aFontMetrics,
        aDrawTarget);

    nscoord offset, baseline;
    aFontMetrics.GetUnderline(offset, mAccessKeyInfo->mAccessUnderlineSize);
    baseline = aFontMetrics.MaxAscent();
    mAccessKeyInfo->mAccessOffset = baseline - offset;
  }
}

nscoord nsTextBoxFrame::CalculateTitleForWidth(gfxContext& aRenderingContext,
                                               nscoord aWidth) {
  DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();

  if (mTitle.IsEmpty()) {
    mCroppedTitle.Truncate();
    return 0;
  }

  RefPtr<nsFontMetrics> fm = nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);

  // see if the text will completely fit in the width given
  nscoord titleWidth = nsLayoutUtils::AppUnitWidthOfStringBidi(
      mTitle, this, *fm, aRenderingContext);
  if (titleWidth <= aWidth) {
    mCroppedTitle = mTitle;
    if (HasRTLChars(mTitle) ||
        StyleVisibility()->mDirection == NS_STYLE_DIRECTION_RTL) {
      AddStateBits(NS_FRAME_IS_BIDI);
    }
    return titleWidth;  // fits, done.
  }

  const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis();
  if (mCropType != CropNone) {
    // start with an ellipsis
    mCroppedTitle.Assign(kEllipsis);

    // see if the width is even smaller than the ellipsis
    // if so, clear the text (XXX set as many '.' as we can?).
    fm->SetTextRunRTL(false);
    titleWidth =
        nsLayoutUtils::AppUnitWidthOfString(kEllipsis, *fm, drawTarget);

    if (titleWidth > aWidth) {
      mCroppedTitle.SetLength(0);
      return 0;
    }

    // if the ellipsis fits perfectly, no use in trying to insert
    if (titleWidth == aWidth) return titleWidth;

    aWidth -= titleWidth;
  } else {
    mCroppedTitle.Truncate(0);
    titleWidth = 0;
  }

  using mozilla::unicode::ClusterIterator;
  using mozilla::unicode::ClusterReverseIterator;

  // ok crop things
  switch (mCropType) {
    case CropAuto:
    case CropNone:
    case CropRight: {
      ClusterIterator iter(mTitle.Data(), mTitle.Length());
      const char16_t* dataBegin = iter;
      const char16_t* pos = dataBegin;
      nscoord charWidth;
      nscoord totalWidth = 0;

      while (!iter.AtEnd()) {
        iter.Next();
        const char16_t* nextPos = iter;
        ptrdiff_t length = nextPos - pos;
        charWidth =
            nsLayoutUtils::AppUnitWidthOfString(pos, length, *fm, drawTarget);
        if (totalWidth + charWidth > aWidth) {
          break;
        }

        if (UTF16_CODE_UNIT_IS_BIDI(*pos)) {
          AddStateBits(NS_FRAME_IS_BIDI);
        }
        pos = nextPos;
        totalWidth += charWidth;
      }

      if (pos == dataBegin) {
        return titleWidth;
      }

      // insert what character we can in.
      nsAutoString title(mTitle);
      title.Truncate(pos - dataBegin);
      mCroppedTitle.Insert(title, 0);
    } break;

    case CropLeft: {
      ClusterReverseIterator iter(mTitle.Data(), mTitle.Length());
      const char16_t* dataEnd = iter;
      const char16_t* prevPos = dataEnd;
      nscoord charWidth;
      nscoord totalWidth = 0;

      while (!iter.AtEnd()) {
        iter.Next();
        const char16_t* pos = iter;
        ptrdiff_t length = prevPos - pos;
        charWidth =
            nsLayoutUtils::AppUnitWidthOfString(pos, length, *fm, drawTarget);
        if (totalWidth + charWidth > aWidth) {
          break;
        }

        if (UTF16_CODE_UNIT_IS_BIDI(*pos)) {
          AddStateBits(NS_FRAME_IS_BIDI);
        }
        prevPos = pos;
        totalWidth += charWidth;
      }

      if (prevPos == dataEnd) {
        return titleWidth;
      }

      nsAutoString copy;
      mTitle.Right(copy, dataEnd - prevPos);
      mCroppedTitle += copy;
    } break;

    case CropCenter: {
      nscoord stringWidth = nsLayoutUtils::AppUnitWidthOfStringBidi(
          mTitle, this, *fm, aRenderingContext);
      if (stringWidth <= aWidth) {
        // the entire string will fit in the maximum width
        mCroppedTitle.Insert(mTitle, 0);
        break;
      }

      // determine how much of the string will fit in the max width
      nscoord charWidth = 0;
      nscoord totalWidth = 0;
      ClusterIterator leftIter(mTitle.Data(), mTitle.Length());
      ClusterReverseIterator rightIter(mTitle.Data(), mTitle.Length());
      const char16_t* dataBegin = leftIter;
      const char16_t* dataEnd = rightIter;
      const char16_t* leftPos = dataBegin;
      const char16_t* rightPos = dataEnd;
      const char16_t* pos;
      ptrdiff_t length;
      nsAutoString leftString, rightString;

      while (leftPos < rightPos) {
        leftIter.Next();
        pos = leftIter;
        length = pos - leftPos;
        charWidth = nsLayoutUtils::AppUnitWidthOfString(leftPos, length, *fm,
                                                        drawTarget);
        if (totalWidth + charWidth > aWidth) {
          break;
        }

        if (UTF16_CODE_UNIT_IS_BIDI(*leftPos)) {
          AddStateBits(NS_FRAME_IS_BIDI);
        }

        leftString.Append(leftPos, length);
        leftPos = pos;
        totalWidth += charWidth;

        if (leftPos >= rightPos) {
          break;
        }

        rightIter.Next();
        pos = rightIter;
        length = rightPos - pos;
        charWidth =
            nsLayoutUtils::AppUnitWidthOfString(pos, length, *fm, drawTarget);
        if (totalWidth + charWidth > aWidth) {
          break;
        }

        if (UTF16_CODE_UNIT_IS_BIDI(*pos)) {
          AddStateBits(NS_FRAME_IS_BIDI);
        }

        rightString.Insert(pos, 0, length);
        rightPos = pos;
        totalWidth += charWidth;
      }

      mCroppedTitle = leftString + kEllipsis + rightString;
    } break;
  }

  return nsLayoutUtils::AppUnitWidthOfStringBidi(mCroppedTitle, this, *fm,
                                                 aRenderingContext);
}

#define OLD_ELLIPSIS NS_LITERAL_STRING("...")

// the following block is to append the accesskey to mTitle if there is an
// accesskey but the mTitle doesn't have the character
void nsTextBoxFrame::UpdateAccessTitle() {
  /*
   * Note that if you change appending access key label spec,
   * you need to maintain same logic in following methods. See bug 324159.
   * toolkit/content/commonDialog.js (setLabelForNode)
   * toolkit/content/widgets/text.xml (formatAccessKey)
   */
  int32_t menuAccessKey;
  nsMenuBarListener::GetMenuAccessKey(&menuAccessKey);
  if (!menuAccessKey || mAccessKey.IsEmpty()) return;

  if (!AlwaysAppendAccessKey() &&
      FindInReadable(mAccessKey, mTitle, nsCaseInsensitiveStringComparator()))
    return;

  nsAutoString accessKeyLabel;
  accessKeyLabel += '(';
  accessKeyLabel += mAccessKey;
  ToUpperCase(accessKeyLabel);
  accessKeyLabel += ')';

  if (mTitle.IsEmpty()) {
    mTitle = accessKeyLabel;
    return;
  }

  const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis();
  uint32_t offset = mTitle.Length();
  if (StringEndsWith(mTitle, kEllipsis)) {
    offset -= kEllipsis.Length();
  } else if (StringEndsWith(mTitle, OLD_ELLIPSIS)) {
    // Try to check with our old ellipsis (for old addons)
    offset -= OLD_ELLIPSIS.Length();
  } else {
    // Try to check with
    // our default ellipsis (for non-localized addons) or ':'
    const char16_t kLastChar = mTitle.Last();
    if (kLastChar == char16_t(0x2026) || kLastChar == char16_t(':')) offset--;
  }

  if (InsertSeparatorBeforeAccessKey() && offset > 0 &&
      !NS_IS_SPACE(mTitle[offset - 1])) {
    mTitle.Insert(' ', offset);
    offset++;
  }

  mTitle.Insert(accessKeyLabel, offset);
}

void nsTextBoxFrame::UpdateAccessIndex() {
  int32_t menuAccessKey;
  nsMenuBarListener::GetMenuAccessKey(&menuAccessKey);
  if (menuAccessKey) {
    if (mAccessKey.IsEmpty()) {
      if (mAccessKeyInfo) {
        delete mAccessKeyInfo;
        mAccessKeyInfo = nullptr;
      }
    } else {
      if (!mAccessKeyInfo) {
        mAccessKeyInfo = new nsAccessKeyInfo();
        if (!mAccessKeyInfo) return;
      }

      nsAString::const_iterator start, end;

      mCroppedTitle.BeginReading(start);
      mCroppedTitle.EndReading(end);

      // remember the beginning of the string
      nsAString::const_iterator originalStart = start;

      bool found;
      if (!AlwaysAppendAccessKey()) {
        // not appending access key - do case-sensitive search
        // first
        found = FindInReadable(mAccessKey, start, end);
        if (!found) {
          // didn't find it - perform a case-insensitive search
          start = originalStart;
          found = FindInReadable(mAccessKey, start, end,
                                 nsCaseInsensitiveStringComparator());
        }
      } else {
        found = RFindInReadable(mAccessKey, start, end,
                                nsCaseInsensitiveStringComparator());
      }

      if (found)
        mAccessKeyInfo->mAccesskeyIndex = Distance(originalStart, start);
      else
        mAccessKeyInfo->mAccesskeyIndex = kNotFound;
    }
  }
}

void nsTextBoxFrame::RecomputeTitle() {
  mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::value, mTitle);

  // This doesn't handle language-specific uppercasing/lowercasing
  // rules, unlike textruns.
  uint8_t textTransform = StyleText()->mTextTransform;
  if (textTransform == NS_STYLE_TEXT_TRANSFORM_UPPERCASE) {
    ToUpperCase(mTitle);
  } else if (textTransform == NS_STYLE_TEXT_TRANSFORM_LOWERCASE) {
    ToLowerCase(mTitle);
  }
  // We can't handle NS_STYLE_TEXT_TRANSFORM_CAPITALIZE because we
  // have no clue about word boundaries here.  We also don't handle
  // NS_STYLE_TEXT_TRANSFORM_FULL_WIDTH.
}

void nsTextBoxFrame::DidSetStyleContext(nsStyleContext* aOldStyleContext) {
  if (!aOldStyleContext) {
    // We're just being initialized
    return;
  }

  const nsStyleText* oldTextStyle = aOldStyleContext->PeekStyleText();
  // We should really have oldTextStyle here, since we asked for our
  // nsStyleText during Init(), but if it's not there for some reason
  // just assume the worst and recompute mTitle.
  if (!oldTextStyle ||
      oldTextStyle->mTextTransform != StyleText()->mTextTransform) {
    RecomputeTitle();
    UpdateAccessTitle();
  }
}

NS_IMETHODIMP
nsTextBoxFrame::DoXULLayout(nsBoxLayoutState& aBoxLayoutState) {
  if (mNeedsReflowCallback) {
    nsIReflowCallback* cb = new nsAsyncAccesskeyUpdate(this);
    if (cb) {
      PresShell()->PostReflowCallback(cb);
    }
    mNeedsReflowCallback = false;
  }

  nsresult rv = nsLeafBoxFrame::DoXULLayout(aBoxLayoutState);

  CalcDrawRect(*aBoxLayoutState.GetRenderingContext());

  const nsStyleText* textStyle = StyleText();

  nsRect scrollBounds(nsPoint(0, 0), GetSize());
  nsRect textRect = mTextDrawRect;

  RefPtr<nsFontMetrics> fontMet =
      nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
  nsBoundingMetrics metrics = fontMet->GetInkBoundsForVisualOverflow(
      mCroppedTitle.get(), mCroppedTitle.Length(),
      aBoxLayoutState.GetRenderingContext()->GetDrawTarget());

  WritingMode wm = GetWritingMode();
  LogicalRect tr(wm, textRect, GetSize());

  tr.IStart(wm) -= metrics.leftBearing;
  tr.ISize(wm) = metrics.width;
  // In DrawText() we always draw with the baseline at MaxAscent() (relative to
  // mTextDrawRect),
  tr.BStart(wm) += fontMet->MaxAscent() - metrics.ascent;
  tr.BSize(wm) = metrics.ascent + metrics.descent;

  textRect = tr.GetPhysicalRect(wm, GetSize());

  // Our scrollable overflow is our bounds; our visual overflow may
  // extend beyond that.
  nsRect visualBounds;
  visualBounds.UnionRect(scrollBounds, textRect);
  nsOverflowAreas overflow(visualBounds, scrollBounds);

  if (textStyle->mTextShadow) {
    // text-shadow extends our visual but not scrollable bounds
    nsRect& vis = overflow.VisualOverflow();
    vis.UnionRect(vis,
                  nsLayoutUtils::GetTextShadowRectsUnion(mTextDrawRect, this));
  }
  FinishAndStoreOverflow(overflow, GetSize());

  return rv;
}

nsRect nsTextBoxFrame::GetComponentAlphaBounds() const {
  if (StyleText()->mTextShadow) {
    return GetVisualOverflowRectRelativeToSelf();
  }
  return mTextDrawRect;
}

bool nsTextBoxFrame::ComputesOwnOverflowArea() { return true; }

/* virtual */ void nsTextBoxFrame::MarkIntrinsicISizesDirty() {
  mNeedsRecalc = true;
  nsLeafBoxFrame::MarkIntrinsicISizesDirty();
}

void nsTextBoxFrame::GetTextSize(gfxContext& aRenderingContext,
                                 const nsString& aString, nsSize& aSize,
                                 nscoord& aAscent) {
  RefPtr<nsFontMetrics> fontMet =
      nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
  aSize.height = fontMet->MaxHeight();
  aSize.width = nsLayoutUtils::AppUnitWidthOfStringBidi(aString, this, *fontMet,
                                                        aRenderingContext);
  aAscent = fontMet->MaxAscent();
}

void nsTextBoxFrame::CalcTextSize(nsBoxLayoutState& aBoxLayoutState) {
  if (mNeedsRecalc) {
    nsSize size;
    gfxContext* rendContext = aBoxLayoutState.GetRenderingContext();
    if (rendContext) {
      GetTextSize(*rendContext, mTitle, size, mAscent);
      if (GetWritingMode().IsVertical()) {
        Swap(size.width, size.height);
      }
      mTextSize = size;
      mNeedsRecalc = false;
    }
  }
}

void nsTextBoxFrame::CalcDrawRect(gfxContext& aRenderingContext) {
  WritingMode wm = GetWritingMode();

  LogicalRect textRect(wm, LogicalPoint(wm, 0, 0), GetLogicalSize(wm));
  nsMargin borderPadding;
  GetXULBorderAndPadding(borderPadding);
  textRect.Deflate(wm, LogicalMargin(wm, borderPadding));

  // determine (cropped) title and underline position
  // determine (cropped) title which fits in aRect, and its width
  // (where "width" is the text measure along its baseline, i.e. actually
  // a physical height in vertical writing modes)
  nscoord titleWidth =
      CalculateTitleForWidth(aRenderingContext, textRect.ISize(wm));

#ifdef ACCESSIBILITY
  // Make sure to update the accessible tree in case when cropped title is
  // changed.
  nsAccessibilityService* accService = GetAccService();
  if (accService) {
    accService->UpdateLabelValue(PresShell(), mContent, mCroppedTitle);
  }
#endif

  // determine if and at which position to put the underline
  UpdateAccessIndex();

  // make the rect as small as our (cropped) text.
  nscoord outerISize = textRect.ISize(wm);
  textRect.ISize(wm) = titleWidth;

  // Align our text within the overall rect by checking our text-align property.
  const nsStyleText* textStyle = StyleText();
  if (textStyle->mTextAlign == NS_STYLE_TEXT_ALIGN_CENTER) {
    textRect.IStart(wm) += (outerISize - textRect.ISize(wm)) / 2;
  } else if (textStyle->mTextAlign == NS_STYLE_TEXT_ALIGN_END ||
             (textStyle->mTextAlign == NS_STYLE_TEXT_ALIGN_LEFT &&
              !wm.IsBidiLTR()) ||
             (textStyle->mTextAlign == NS_STYLE_TEXT_ALIGN_RIGHT &&
              wm.IsBidiLTR())) {
    textRect.IStart(wm) += (outerISize - textRect.ISize(wm));
  }

  mTextDrawRect = textRect.GetPhysicalRect(wm, GetSize());
}

/**
 * Ok return our dimensions
 */
nsSize nsTextBoxFrame::GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) {
  CalcTextSize(aBoxLayoutState);

  nsSize size = mTextSize;
  DISPLAY_PREF_SIZE(this, size);

  AddBorderAndPadding(size);
  bool widthSet, heightSet;
  nsIFrame::AddXULPrefSize(this, size, widthSet, heightSet);

  return size;
}

/**
 * Ok return our dimensions
 */
nsSize nsTextBoxFrame::GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) {
  CalcTextSize(aBoxLayoutState);

  nsSize size = mTextSize;
  DISPLAY_MIN_SIZE(this, size);

  // if there is cropping our min width becomes our border and padding
  if (mCropType != CropNone && mCropType != CropAuto) {
    if (GetWritingMode().IsVertical()) {
      size.height = 0;
    } else {
      size.width = 0;
    }
  }

  AddBorderAndPadding(size);
  bool widthSet, heightSet;
  nsIFrame::AddXULMinSize(aBoxLayoutState, this, size, widthSet, heightSet);

  return size;
}

nscoord nsTextBoxFrame::GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState) {
  CalcTextSize(aBoxLayoutState);

  nscoord ascent = mAscent;

  nsMargin m(0, 0, 0, 0);
  GetXULBorderAndPadding(m);

  WritingMode wm = GetWritingMode();
  ascent += LogicalMargin(wm, m).BStart(wm);

  return ascent;
}

#ifdef DEBUG_FRAME_DUMP
nsresult nsTextBoxFrame::GetFrameName(nsAString& aResult) const {
  MakeFrameName(NS_LITERAL_STRING("TextBox"), aResult);
  aResult += NS_LITERAL_STRING("[value=") + mTitle + NS_LITERAL_STRING("]");
  return NS_OK;
}
#endif

// If you make changes to this function, check its counterparts
// in nsBoxFrame and nsXULLabelFrame
nsresult nsTextBoxFrame::RegUnregAccessKey(bool aDoReg) {
  // if we have no content, we can't do anything
  if (!mContent) return NS_ERROR_FAILURE;

  // check if we have a |control| attribute
  // do this check first because few elements have control attributes, and we
  // can weed out most of the elements quickly.

  // XXXjag a side-effect is that we filter out anonymous <label>s
  // in e.g. <menu>, <menuitem>, <button>. These <label>s inherit
  // |accesskey| and would otherwise register themselves, overwriting
  // the content we really meant to be registered.
  if (!mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::control))
    return NS_OK;

  // see if we even have an access key
  nsAutoString accessKey;
  mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
                                 accessKey);

  if (accessKey.IsEmpty()) return NS_OK;

  // With a valid PresContext we can get the ESM
  // and (un)register the access key
  EventStateManager* esm = PresContext()->EventStateManager();

  uint32_t key = accessKey.First();
  if (aDoReg)
    esm->RegisterAccessKey(mContent->AsElement(), key);
  else
    esm->UnregisterAccessKey(mContent->AsElement(), key);

  return NS_OK;
}