fune/layout/xul/nsTextBoxFrame.cpp
Ting-Yu Lin 653f4b3694 Bug 1745113 Part 5 - Make grapheme cluster break iterators implement SegmentIteratorUtf16, and adapt the callers. r=necko-reviewers,jfkthame,kershaw
This is the main patch for the bug. It aims to change the grapheme cluster
break's `Next()` API by implementing SegmentIteratorUtf16 interface, and adapt
the callers. It shouldn't change the behavior.

While rewriting the caller, one caveat worth mentioning is the loop termination
condition. If the old code relies on `!AtEnd()` as the loop termination
condition, and it advances the iterator at the end of the loop, it meant
to *skip* its logic when the break position is at the end of the string. For
example, see the `mozTXTToHTMLConv::NumberOfMatches`.

This patch also hooks grapheme cluster break iterator into
Segmenter::TryCreate() interface.

Existing test coverage for the file changed:
- netwerk/test/unit/test_mozTXTToHTMLConv.js
- layout/reftests/forms/input/file/dynamic-max-width.html

Differential Revision: https://phabricator.services.mozilla.com/D135643
2022-01-13 18:36:04 +00:00

1081 lines
35 KiB
C++

/* -*- 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::StyleChange,
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::StyleChange,
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;
uint8_t overStyle = 0;
uint8_t underStyle = 0;
uint8_t strikeStyle = 0;
// 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);
}
uint8_t 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 != NS_STYLE_TEXT_DECORATION_STYLE_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 != NS_STYLE_TEXT_DECORATION_STYLE_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 != 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 = 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;
}
}
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 == StyleDirection::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::intl::GraphemeClusterBreakIteratorUtf16;
using mozilla::intl::GraphemeClusterBreakReverseIteratorUtf16;
// ok crop things
switch (mCropType) {
case CropAuto:
case CropNone:
case CropRight: {
const Span title(mTitle);
GraphemeClusterBreakIteratorUtf16 iter(title);
uint32_t pos = 0;
nscoord totalWidth = 0;
while (Maybe<uint32_t> nextPos = iter.Next()) {
const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString(
title.FromTo(pos, *nextPos), *fm, drawTarget);
if (totalWidth + charWidth > aWidth) {
break;
}
if (UTF16_CODE_UNIT_IS_BIDI(mTitle[pos])) {
AddStateBits(NS_FRAME_IS_BIDI);
}
pos = *nextPos;
totalWidth += charWidth;
}
if (pos == 0) {
return titleWidth;
}
// insert what character we can in.
mCroppedTitle.Insert(title.To(pos), 0);
} break;
case CropLeft: {
const Span title(mTitle);
GraphemeClusterBreakReverseIteratorUtf16 iter(title);
uint32_t pos = title.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(
title.FromTo(*nextPos, pos), *fm, drawTarget);
if (totalWidth + charWidth > aWidth) {
break;
}
if (UTF16_CODE_UNIT_IS_BIDI(mTitle[*nextPos])) {
AddStateBits(NS_FRAME_IS_BIDI);
}
pos = *nextPos;
totalWidth += charWidth;
}
if (pos == title.Length()) {
return titleWidth;
}
mCroppedTitle.Append(title.From(pos));
} 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
const Span title(mTitle);
nscoord totalWidth = 0;
GraphemeClusterBreakIteratorUtf16 leftIter(title);
GraphemeClusterBreakReverseIteratorUtf16 rightIter(title);
uint32_t leftPos = 0;
uint32_t rightPos = title.Length();
nsAutoString leftString, rightString;
while (leftPos < rightPos) {
Maybe<uint32_t> nextPos = leftIter.Next();
Span chars = title.FromTo(leftPos, *nextPos);
nscoord charWidth =
nsLayoutUtils::AppUnitWidthOfString(chars, *fm, drawTarget);
if (totalWidth + charWidth > aWidth) {
break;
}
if (UTF16_CODE_UNIT_IS_BIDI(mTitle[leftPos])) {
AddStateBits(NS_FRAME_IS_BIDI);
}
leftString.Append(chars);
leftPos = *nextPos;
totalWidth += charWidth;
if (leftPos >= rightPos) {
break;
}
nextPos = rightIter.Next();
chars = title.FromTo(*nextPos, rightPos);
charWidth = nsLayoutUtils::AppUnitWidthOfString(chars, *fm, drawTarget);
if (totalWidth + charWidth > aWidth) {
break;
}
if (UTF16_CODE_UNIT_IS_BIDI(mTitle[*nextPos])) {
AddStateBits(NS_FRAME_IS_BIDI);
}
rightString.Insert(chars, 0);
rightPos = *nextPos;
totalWidth += charWidth;
}
mCroppedTitle = leftString + kEllipsis + rightString;
} break;
}
return nsLayoutUtils::AppUnitWidthOfStringBidi(mCroppedTitle, this, *fm,
aRenderingContext);
}
#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(&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;
}
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(&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.
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