diff options
Diffstat (limited to 'layout/xul/nsTextBoxFrame.cpp')
-rw-r--r-- | layout/xul/nsTextBoxFrame.cpp | 1055 |
1 files changed, 1055 insertions, 0 deletions
diff --git a/layout/xul/nsTextBoxFrame.cpp b/layout/xul/nsTextBoxFrame.cpp new file mode 100644 index 0000000000..d31b8418da --- /dev/null +++ b/layout/xul/nsTextBoxFrame.cpp @@ -0,0 +1,1055 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 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/intl/BidiEmbeddingLevel.h" +#include "mozilla/Attributes.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/intl/Segmenter.h" +#include "mozilla/layers/RenderRootStateManager.h" +#include "mozilla/gfx/2D.h" +#include "nsFontMetrics.h" +#include "nsReadableUtils.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsGkAtoms.h" +#include "nsPresContext.h" +#include "gfxContext.h" +#include "nsIContent.h" +#include "nsNameSpaceManager.h" +#include "nsBoxLayoutState.h" +#include "nsMenuBarListener.h" +#include "nsString.h" +#include "nsITheme.h" +#include "nsUnicharUtils.h" +#include "nsContentUtils.h" +#include "nsDisplayList.h" +#include "nsCSSRendering.h" +#include "nsIReflowCallback.h" +#include "nsBoxFrame.h" +#include "nsLayoutUtils.h" +#include "TextDrawTarget.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(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsTextBoxFrame(aStyle, aPresShell->GetPresContext()); +} + +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, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); + } else if (aRedraw) { + nsBoxLayoutState state(PresContext()); + XULRedraw(state); + } + + return NS_OK; +} + +nsTextBoxFrame::nsTextBoxFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsLeafBoxFrame(aStyle, aPresContext, 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 */ +} + +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; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, + accesskey); + + if (!accesskey.Equals(mAccessKey)) { + // Need to get clean mTitle. + RecomputeTitle(); + mAccessKey = accesskey; + UpdateAccessTitle(); + PresShell()->FrameNeedsReflow( + this, IntrinsicDirty::FrameAncestorsAndDescendants, 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 dom::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; + } +} + +namespace mozilla { + +class nsDisplayXULTextBox final : public nsPaintedDisplayItem { + public: + nsDisplayXULTextBox(nsDisplayListBuilder* aBuilder, nsTextBoxFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayXULTextBox); + } +#ifdef NS_BUILD_REFCNT_LOGGING + MOZ_COUNTED_DTOR_OVERRIDE(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); + + virtual bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; +}; + +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) { + // Paint the text shadow before doing any foreground stuff + nsRect drawRect = + static_cast<nsTextBoxFrame*>(mFrame)->mTextDrawRect + ToReferenceFrame(); + nsLayoutUtils::PaintTextShadow(mFrame, aCtx, drawRect, + GetPaintRect(aBuilder, aCtx), + mFrame->StyleText()->mColor.ToColor(), + 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, mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame(), + ToReferenceFrame() + aOffset, aColor); +} + +bool nsDisplayXULTextBox::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + bool snap = false; + auto bounds = GetBounds(aDisplayListBuilder, &snap); + + if (bounds.IsEmpty()) { + return true; + } + + auto appUnitsPerDevPixel = Frame()->PresContext()->AppUnitsPerDevPixel(); + gfx::Point deviceOffset = + LayoutDevicePoint::FromAppUnits(bounds.TopLeft(), appUnitsPerDevPixel) + .ToUnknownPoint(); + + RefPtr<mozilla::layout::TextDrawTarget> textDrawer = + new mozilla::layout::TextDrawTarget(aBuilder, aResources, aSc, aManager, + this, bounds); + RefPtr<gfxContext> captureCtx = + gfxContext::CreateOrNull(textDrawer, deviceOffset); + + Paint(aDisplayListBuilder, captureCtx); + textDrawer->TerminateShadows(); + + return textDrawer->Finish(); +} + +nsRect nsDisplayXULTextBox::GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const { + *aSnap = false; + return mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame(); +} + +nsRect nsDisplayXULTextBox::GetComponentAlphaBounds( + nsDisplayListBuilder* aBuilder) const { + return static_cast<nsTextBoxFrame*>(mFrame)->GetComponentAlphaBounds() + + ToReferenceFrame(); +} + +} // namespace mozilla + +void nsTextBoxFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (!IsVisibleForPainting()) return; + + nsLeafBoxFrame::BuildDisplayList(aBuilder, aLists); + + aLists.Content()->AppendNewToTop<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; + auto overStyle = StyleTextDecorationStyle::None; + auto underStyle = StyleTextDecorationStyle::None; + auto strikeStyle = StyleTextDecorationStyle::None; + + // Begin with no decorations + auto decorations = StyleTextDecorationLine::NONE; + // A mask of all possible line decorations. + auto decorMask = StyleTextDecorationLine::UNDERLINE | + StyleTextDecorationLine::OVERLINE | + StyleTextDecorationLine::LINE_THROUGH; + + WritingMode wm = GetWritingMode(); + bool vertical = wm.IsVertical(); + + nsIFrame* f = this; + do { // find decoration colors + ComputedStyle* context = f->Style(); + if (!context->HasTextDecorationLines()) { + break; + } + const nsStyleTextReset* styleText = context->StyleTextReset(); + + // a decoration defined here + if (decorMask & styleText->mTextDecorationLine) { + nscolor color; + if (aOverrideColor) { + color = *aOverrideColor; + } else { + color = styleText->mTextDecorationColor.CalcColor(*context); + } + const auto style = styleText->mTextDecorationStyle; + + if (StyleTextDecorationLine::UNDERLINE & decorMask & + styleText->mTextDecorationLine) { + underColor = color; + underStyle = style; + decorMask &= ~StyleTextDecorationLine::UNDERLINE; + decorations |= StyleTextDecorationLine::UNDERLINE; + } + if (StyleTextDecorationLine::OVERLINE & decorMask & + styleText->mTextDecorationLine) { + overColor = color; + overStyle = style; + decorMask &= ~StyleTextDecorationLine::OVERLINE; + decorations |= StyleTextDecorationLine::OVERLINE; + } + if (StyleTextDecorationLine::LINE_THROUGH & decorMask & + styleText->mTextDecorationLine) { + strikeColor = color; + strikeStyle = style; + decorMask &= ~StyleTextDecorationLine::LINE_THROUGH; + decorations |= StyleTextDecorationLine::LINE_THROUGH; + } + } + } while (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 & (StyleTextDecorationLine::OVERLINE | + StyleTextDecorationLine::UNDERLINE)) { + fontMet->GetUnderline(offset, size); + params.lineSize.height = presContext->AppUnitsToGfxUnits(size); + if ((decorations & StyleTextDecorationLine::UNDERLINE) && + underStyle != StyleTextDecorationStyle::None) { + params.color = underColor; + params.offset = presContext->AppUnitsToGfxUnits(offset); + params.decoration = StyleTextDecorationLine::UNDERLINE; + params.style = underStyle; + nsCSSRendering::PaintDecorationLine(this, *drawTarget, params); + } + if ((decorations & StyleTextDecorationLine::OVERLINE) && + overStyle != StyleTextDecorationStyle::None) { + params.color = overColor; + params.offset = params.ascent; + params.decoration = StyleTextDecorationLine::OVERLINE; + params.style = overStyle; + nsCSSRendering::PaintDecorationLine(this, *drawTarget, params); + } + } + + RefPtr<gfxContext> refContext = + PresShell()->CreateReferenceRenderingContext(); + DrawTarget* refDrawTarget = refContext->GetDrawTarget(); + + CalculateUnderline(refDrawTarget, *fontMet); + + DeviceColor color = ToDeviceColor( + aOverrideColor ? *aOverrideColor : StyleText()->mColor.ToColor()); + ColorPattern colorPattern(color); + aRenderingContext.SetDeviceColor(color); + + nsresult rv = NS_ERROR_FAILURE; + + if (mState & NS_FRAME_IS_BIDI) { + presContext->SetBidiEnabled(); + mozilla::intl::BidiEmbeddingLevel level = + nsBidiPresUtils::BidiLevelFromStyle(Style()); + 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, colorPattern); + } + + // 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 & StyleTextDecorationLine::LINE_THROUGH) && + strikeStyle != StyleTextDecorationStyle::None) { + fontMet->GetStrikeout(offset, size); + params.color = strikeColor; + params.lineSize.height = presContext->AppUnitsToGfxUnits(size); + params.offset = presContext->AppUnitsToGfxUnits(offset); + params.decoration = StyleTextDecorationLine::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; + } +} + +void nsTextBoxFrame::CropStringForWidth(nsAString& aText, + gfxContext& aRenderingContext, + nsFontMetrics& aFontMetrics, + nscoord aWidth, + CroppingStyle aCropType) { + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + + // See if the width is even smaller than the ellipsis + // If so, clear the text completely. + const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis(); + aFontMetrics.SetTextRunRTL(false); + nscoord ellipsisWidth = + nsLayoutUtils::AppUnitWidthOfString(kEllipsis, aFontMetrics, drawTarget); + + if (ellipsisWidth > aWidth) { + aText.Truncate(0); + return; + } + if (ellipsisWidth == aWidth) { + aText.Assign(kEllipsis); + return; + } + + // We will be drawing an ellipsis, thank you very much. + // Subtract out the required width of the ellipsis. + // This is the total remaining width we have to play with. + aWidth -= ellipsisWidth; + + using mozilla::intl::GraphemeClusterBreakIteratorUtf16; + using mozilla::intl::GraphemeClusterBreakReverseIteratorUtf16; + + // Now we crop. This is quite basic: it will not be really accurate in the + // presence of complex scripts with contextual shaping, etc., as it measures + // each grapheme cluster in isolation, not in its proper context. + switch (aCropType) { + case CropAuto: + case CropNone: + case CropRight: { + const Span text(aText); + GraphemeClusterBreakIteratorUtf16 iter(text); + uint32_t pos = 0; + nscoord totalWidth = 0; + + while (Maybe<uint32_t> nextPos = iter.Next()) { + const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(pos, *nextPos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + pos = *nextPos; + totalWidth += charWidth; + } + + if (pos < aText.Length()) { + aText.Replace(pos, aText.Length() - pos, kEllipsis); + } + } break; + + case CropLeft: { + const Span text(aText); + GraphemeClusterBreakReverseIteratorUtf16 iter(text); + uint32_t pos = text.Length(); + nscoord totalWidth = 0; + + // nextPos is decreasing since we use a reverse iterator. + while (Maybe<uint32_t> nextPos = iter.Next()) { + const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(*nextPos, pos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + + pos = *nextPos; + totalWidth += charWidth; + } + + if (pos > 0) { + aText.Replace(0, pos, kEllipsis); + } + } break; + + case CropCenter: { + const Span text(aText); + nscoord totalWidth = 0; + GraphemeClusterBreakIteratorUtf16 leftIter(text); + GraphemeClusterBreakReverseIteratorUtf16 rightIter(text); + uint32_t leftPos = 0; + uint32_t rightPos = text.Length(); + + while (leftPos < rightPos) { + Maybe<uint32_t> nextPos = leftIter.Next(); + nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(leftPos, *nextPos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + + leftPos = *nextPos; + totalWidth += charWidth; + + if (leftPos >= rightPos) { + break; + } + + nextPos = rightIter.Next(); + charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(*nextPos, rightPos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + + rightPos = *nextPos; + totalWidth += charWidth; + } + + if (leftPos < rightPos) { + aText.Replace(leftPos, rightPos - leftPos, kEllipsis); + } + } break; + } +} + +nscoord nsTextBoxFrame::CalculateTitleForWidth(gfxContext& aRenderingContext, + nscoord aMaxWidth) { + if (mTitle.IsEmpty()) { + mCroppedTitle.Truncate(); + return 0; + } + + RefPtr<nsFontMetrics> fm = nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + + // See if the text needs to be cropped to fit in the width given. + mCroppedTitle = mTitle; + nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi( + mCroppedTitle, this, *fm, aRenderingContext); + if (width > aMaxWidth && mCropType != CropNone) { + CropStringForWidth(mCroppedTitle, aRenderingContext, *fm, aMaxWidth, + mCropType); + width = nsLayoutUtils::AppUnitWidthOfStringBidi(mCroppedTitle, this, *fm, + aRenderingContext); + } + + if (StyleVisibility()->mDirection == StyleDirection::Rtl || + HasRTLChars(mCroppedTitle)) { + AddStateBits(NS_FRAME_IS_BIDI); + } + + return width; +} + +#define OLD_ELLIPSIS u"..."_ns + +// 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/components/prompts/src/CommonDialog.jsm (setLabelForNode) + * toolkit/content/widgets/text.js (formatAccessKey) + */ + int32_t menuAccessKey = nsMenuBarListener::GetMenuAccessKey(); + 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; + } + + if (StringEndsWith(mTitle, accessKeyLabel)) { + // Never append another "(X)" if the title already ends with "(X)". + 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(); + 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. + StyleTextTransform textTransform = StyleText()->mTextTransform; + if (textTransform.case_ == StyleTextTransformCase::Uppercase) { + ToUpperCase(mTitle); + } else if (textTransform.case_ == StyleTextTransformCase::Lowercase) { + ToLowerCase(mTitle); + } + // We can't handle StyleTextTransformCase::Capitalize because we + // have no clue about word boundaries here. We also don't handle + // the full-width or full-size-kana transforms. +} + +void nsTextBoxFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) { + nsLeafBoxFrame::DidSetComputedStyle(aOldComputedStyle); + + if (!aOldComputedStyle) { + // We're just being initialized + return; + } + + const nsStyleText* oldTextStyle = aOldComputedStyle->StyleText(); + if (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->GetInkBoundsForInkOverflow( + 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 ink overflow may + // extend beyond that. + nsRect visualBounds; + visualBounds.UnionRect(scrollBounds, textRect); + OverflowAreas overflow(visualBounds, scrollBounds); + + if (textStyle->HasTextShadow()) { + // text-shadow extends our visual but not scrollable bounds + nsRect& vis = overflow.InkOverflow(); + vis.UnionRect(vis, + nsLayoutUtils::GetTextShadowRectsUnion(mTextDrawRect, this)); + } + FinishAndStoreOverflow(overflow, GetSize()); + + return rv; +} + +nsRect nsTextBoxFrame::GetComponentAlphaBounds() const { + if (StyleText()->HasTextShadow()) { + return InkOverflowRectRelativeToSelf(); + } + return mTextDrawRect; +} + +bool nsTextBoxFrame::XULComputesOwnOverflowArea() { 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()) { + std::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 == StyleTextAlign::Center) { + textRect.IStart(wm) += (outerISize - textRect.ISize(wm)) / 2; + } else if (textStyle->mTextAlign == StyleTextAlign::End || + (textStyle->mTextAlign == StyleTextAlign::Left && + wm.IsBidiRTL()) || + (textStyle->mTextAlign == StyleTextAlign::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); + + AddXULBorderAndPadding(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; + } + } + + AddXULBorderAndPadding(size); + bool widthSet, heightSet; + nsIFrame::AddXULMinSize(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(u"TextBox"_ns, aResult); + aResult += u"[value="_ns + mTitle + u"]"_ns; + return NS_OK; +} +#endif |