Files
sousa-gecko/layout/forms/nsTextControlFrame.cpp

329 lines
12 KiB
C++

/* 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 "nsTextControlFrame.h"
#include <algorithm>
#include "ErrorList.h"
#include "PseudoStyleType.h"
#include "gfxContext.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/IMEContentObserver.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/PresShell.h"
#include "mozilla/ReflowInput.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/TextEditor.h"
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/dom/HTMLTextAreaElement.h"
#include "mozilla/dom/Selection.h"
#include "nsCaret.h"
#include "nsContentUtils.h"
#include "nsDisplayList.h"
#include "nsFocusManager.h"
#include "nsFontMetrics.h"
#include "nsFrameSelection.h"
#include "nsGenericHTMLElement.h"
#include "nsIContent.h"
#include "nsIEditor.h"
#include "nsINode.h"
#include "nsLayoutUtils.h"
#include "nsPresContext.h"
using namespace mozilla;
using namespace mozilla::dom;
nsTextControlFrame* NS_NewTextControlFrame(PresShell* aPresShell,
ComputedStyle* aStyle) {
return new (aPresShell)
nsTextControlFrame(aStyle, aPresShell->GetPresContext());
}
NS_IMPL_FRAMEARENA_HELPERS(nsTextControlFrame)
NS_QUERYFRAME_HEAD(nsTextControlFrame)
NS_QUERYFRAME_ENTRY(nsTextControlFrame)
NS_QUERYFRAME_TAIL_INHERITING(ScrollContainerFrame)
#ifdef ACCESSIBILITY
a11y::AccType nsTextControlFrame::AccessibleType() {
if (ControlElement()->ControlType() == FormControlType::InputNumber) {
return a11y::eHTMLSpinnerType;
}
return a11y::eHTMLTextFieldType;
}
#endif
nsTextControlFrame::nsTextControlFrame(ComputedStyle* aStyle,
nsPresContext* aPresContext)
: ScrollContainerFrame(aStyle, aPresContext, kClassID,
/* aIsRoot = */ false) {}
nsTextControlFrame::~nsTextControlFrame() = default;
Element* nsTextControlFrame::GetButton() const { return mButtonContent; }
nsIFrame* nsTextControlFrame::GetButtonBoxFrame() const {
return mButtonContent ? mButtonContent->GetPrimaryFrame() : nullptr;
}
nsresult nsTextControlFrame::CreateAnonymousContent(
nsTArray<ContentInfo>& aElements) {
nsresult rv = ScrollContainerFrame::CreateAnonymousContent(aElements);
NS_ENSURE_SUCCESS(rv, rv);
mButtonContent = ControlElement()->CreateButton();
if (mButtonContent) {
aElements.AppendElement(mButtonContent);
}
return NS_OK;
}
void nsTextControlFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) {
switch (StyleUIReset()->mFieldSizing) {
case StyleFieldSizing::Content:
RemoveStateBits(NS_FRAME_REFLOW_ROOT);
break;
case StyleFieldSizing::Fixed:
// Mark the input as being a reflow root. This will allow incremental
// reflows to be initiated at this frame, rather than descending from the
// root frame of the frame hierarchy.
AddStateBits(NS_FRAME_REFLOW_ROOT);
break;
}
ScrollContainerFrame::DidSetComputedStyle(aOldComputedStyle);
}
void nsTextControlFrame::AppendAnonymousContentTo(
nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
ScrollContainerFrame::AppendAnonymousContentTo(aElements, aFilter);
if (mButtonContent) {
aElements.AppendElement(mButtonContent);
}
}
void nsTextControlFrame::InitPrimaryFrame() {
if (auto* ts = ControlElement()->GetTextControlState()) {
ts->InitializeSelection(PresShell());
}
nsIFrame::InitPrimaryFrame();
}
void nsTextControlFrame::Destroy(DestroyContext& aContext) {
if (auto* ts = ControlElement()->GetTextControlState()) {
ts->DeinitSelection();
}
aContext.AddAnonymousContent(mButtonContent.forget());
ScrollContainerFrame::Destroy(aContext);
}
LogicalSize nsTextControlFrame::CalcFixedSize(gfxContext* aRenderingContext,
WritingMode aWM) const {
MOZ_ASSERT(StyleUIReset()->mFieldSizing == StyleFieldSizing::Fixed);
LogicalSize intrinsicSize(aWM);
const float inflation = nsLayoutUtils::FontSizeInflationFor(this);
RefPtr<nsFontMetrics> fontMet =
nsLayoutUtils::GetFontMetricsForFrame(this, inflation);
const nscoord lineHeight = ReflowInput::CalcLineHeight(
*Style(), PresContext(), GetContent(), inflation);
// Use the larger of the font's "average" char width or the width of the
// zero glyph (if present) as the basis for resolving the size attribute.
const nscoord charWidth =
std::max(fontMet->ZeroOrAveCharWidth(), fontMet->AveCharWidth());
const nscoord charMaxAdvance = fontMet->MaxAdvance();
// Initialize based on the width in characters.
const Maybe<int32_t> maybeCols = GetCols();
const int32_t cols = maybeCols.valueOr(TextControlElement::DEFAULT_COLS);
intrinsicSize.ISize(aWM) = cols * charWidth;
// If we do not have what appears to be a fixed-width font, add a "slop"
// amount based on the max advance of the font (clamped to twice charWidth,
// because some fonts have a few extremely-wide outliers that would result
// in excessive width here; e.g. the triple-emdash ligature in SFNS Text),
// minus 4px. This helps avoid input fields becoming unusably narrow with
// small size values.
if (charMaxAdvance - charWidth > AppUnitsPerCSSPixel()) {
nscoord internalPadding =
std::max(0, std::min(charMaxAdvance, charWidth * 2) -
nsPresContext::CSSPixelsToAppUnits(4));
internalPadding = RoundToMultiple(internalPadding, AppUnitsPerCSSPixel());
intrinsicSize.ISize(aWM) += internalPadding;
}
// Increment width with cols * letter-spacing.
{
const auto& letterSpacing = StyleText()->mLetterSpacing;
if (!letterSpacing.IsDefinitelyZero()) {
intrinsicSize.ISize(aWM) +=
cols * letterSpacing.Resolve(fontMet->EmHeight());
}
}
// Set the height equal to total number of rows (times the height of each
// line, of course)
intrinsicSize.BSize(aWM) = lineHeight * GetRows();
// Add in the size of the scrollbars for textarea
if (IsTextArea()) {
if (ScrollContainerFrame* scrollContainerFrame = GetScrollTargetFrame()) {
LogicalMargin scrollbarSizes(
aWM, scrollContainerFrame->GetDesiredScrollbarSizes());
intrinsicSize.ISize(aWM) += scrollbarSizes.IStartEnd(aWM);
// We only include scrollbar-thickness in our BSize if the scrollbar on
// that side is explicitly forced-to-be-present.
const bool includeScrollbarBSize = [&] {
if (!StaticPrefs::
layout_forms_textarea_sizing_excludes_auto_scrollbar_enabled()) {
return true;
}
auto overflow = aWM.IsVertical() ? StyleDisplay()->mOverflowY
: StyleDisplay()->mOverflowX;
return overflow == StyleOverflow::Scroll;
}();
if (includeScrollbarBSize) {
intrinsicSize.BSize(aWM) += scrollbarSizes.BStartEnd(aWM);
}
}
}
// Add the inline size of the button if our char size is explicit, so as to
// make sure to make enough space for it.
if (maybeCols.isSome()) {
if (auto* button = GetButton(); button && button->GetPrimaryFrame()) {
const IntrinsicSizeInput input(aRenderingContext, Nothing(), Nothing());
intrinsicSize.ISize(aWM) += button->GetPrimaryFrame()->GetMinISize(input);
}
}
return intrinsicSize;
}
nscoord nsTextControlFrame::IntrinsicISize(const IntrinsicSizeInput& aInput,
IntrinsicISizeType aType) {
if (StyleUIReset()->mFieldSizing == StyleFieldSizing::Content) {
return ScrollContainerFrame::IntrinsicISize(aInput, aType);
}
// Our min inline size is just our preferred inline-size if we have auto
// inline size.
WritingMode wm = GetWritingMode();
return CalcFixedSize(aInput.mContext, wm).ISize(wm);
}
Maybe<nscoord> nsTextControlFrame::ComputeBaseline(
const nsIFrame* aFrame, const ReflowInput& aReflowInput,
bool aForSingleLineControl) {
// If we're layout-contained, we have no baseline.
if (aReflowInput.mStyleDisplay->IsContainLayout()) {
return Nothing();
}
WritingMode wm = aReflowInput.GetWritingMode();
nscoord lineHeight = aReflowInput.ComputedBSize();
if (!aForSingleLineControl || lineHeight == NS_UNCONSTRAINEDSIZE) {
lineHeight = aReflowInput.ApplyMinMaxBSize(aReflowInput.GetLineHeight());
}
RefPtr<nsFontMetrics> fontMet =
nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame);
return Some(nsLayoutUtils::GetCenteredFontBaseline(fontMet, lineHeight,
wm.IsLineInverted()) +
aReflowInput.ComputedLogicalBorderPadding(wm).BStart(wm));
}
void nsTextControlFrame::Reflow(nsPresContext* aPresContext,
ReflowOutput& aDesiredSize,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
DO_GLOBAL_REFLOW_COUNT("nsTextControlFrame");
MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
MOZ_ASSERT_IF(StyleUIReset()->mFieldSizing == StyleFieldSizing::Fixed,
HasAnyStateBits(NS_FRAME_REFLOW_ROOT));
{
// Calculate the baseline and store it in mFirstBaseline.
// TODO(emilio): Is this still needed?
auto baseline =
ComputeBaseline(this, aReflowInput, IsSingleLineTextControl());
mFirstBaseline = baseline.valueOr(NS_INTRINSIC_ISIZE_UNKNOWN);
if (baseline) {
aDesiredSize.SetBlockStartAscent(*baseline);
}
}
// FIXME(emilio): This is rather hacky, but matches how nsBlockFrame tweaks
// avail bsize. Maybe do a copy instead, or plumb stuff further down but...
const auto oldBSize = aReflowInput.ComputedBSize();
const WritingMode wm = aReflowInput.GetWritingMode();
if (oldBSize == NS_UNCONSTRAINEDSIZE &&
StyleUIReset()->mFieldSizing != StyleFieldSizing::Content) {
const nscoord fixedBSize = aReflowInput.ApplyMinMaxBSize(
CalcFixedSize(aReflowInput.mRenderingContext, wm).BSize(wm));
const_cast<ReflowInput&>(aReflowInput)
.SetComputedBSize(fixedBSize, ReflowInput::ResetResizeFlags::No);
}
ScrollContainerFrame::Reflow(aPresContext, aDesiredSize, aReflowInput,
aStatus);
const_cast<ReflowInput&>(aReflowInput)
.SetComputedBSize(oldBSize, ReflowInput::ResetResizeFlags::No);
}
void nsTextControlFrame::HandleReadonlyOrDisabledChange() {
RefPtr<TextControlElement> el = ControlElement();
const RefPtr<TextEditor> editor = el->GetExtantTextEditor();
if (!editor) {
return;
}
nsISelectionController* const selCon = el->GetSelectionController();
if (!selCon) {
return;
}
if (el->IsDisabledOrReadOnly()) {
if (nsFocusManager::GetFocusedElementStatic() == el) {
selCon->SetCaretEnabled(false);
}
editor->AddFlags(nsIEditor::eEditorReadonlyMask);
} else {
if (nsFocusManager::GetFocusedElementStatic() == el) {
selCon->SetCaretEnabled(true);
}
editor->RemoveFlags(nsIEditor::eEditorReadonlyMask);
}
}
void nsTextControlFrame::ElementStateChanged(dom::ElementState aStates) {
if (aStates.HasAtLeastOneOfStates(dom::ElementState::READONLY |
dom::ElementState::DISABLED)) {
HandleReadonlyOrDisabledChange();
}
return ScrollContainerFrame::ElementStateChanged(aStates);
}
nsresult nsTextControlFrame::PeekOffset(PeekOffsetStruct* aPos) {
return NS_ERROR_FAILURE;
}
Maybe<nscoord> nsTextControlFrame::GetNaturalBaselineBOffset(
mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup,
BaselineExportContext aExportContext) const {
if (!IsSingleLineTextControl()) {
if (StyleDisplay()->IsContainLayout()) {
return Nothing{};
}
if (aBaselineGroup == BaselineSharingGroup::First) {
return Some(CSSMinMax(mFirstBaseline, 0, BSize(aWM)));
}
// This isn't great, but the content of the root NAC isn't guaranteed
// to be loaded, so the best we can do is the edge of the border-box.
if (aWM.IsCentralBaseline()) {
return Some(BSize(aWM) / 2);
}
return Some(0);
}
NS_ASSERTION(!IsSubtreeDirty(), "frame must not be dirty");
return GetSingleLineTextControlBaseline(this, mFirstBaseline, aWM,
aBaselineGroup);
}