Source code

Revision control

Copy as Markdown

Other Tools

/* 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 "nsListControlFrame.h"
#include <algorithm>
#include "mozilla/Attributes.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/PresShell.h"
#include "mozilla/ReflowInput.h"
#include "mozilla/dom/HTMLOptGroupElement.h"
#include "mozilla/dom/HTMLOptionsCollection.h"
#include "mozilla/dom/HTMLSelectElement.h"
#include "nsCSSRendering.h"
#include "nsComboboxControlFrame.h"
#include "nsContentUtils.h"
#include "nsDisplayList.h"
#include "nsFontMetrics.h"
#include "nsGkAtoms.h"
#include "nsLayoutUtils.h"
#include "nscore.h"
using namespace mozilla;
using namespace mozilla::dom;
//---------------------------------------------------------
nsListControlFrame* NS_NewListControlFrame(PresShell* aPresShell,
ComputedStyle* aStyle) {
return new (aPresShell)
nsListControlFrame(aStyle, aPresShell->GetPresContext());
}
NS_IMPL_FRAMEARENA_HELPERS(nsListControlFrame)
nsListControlFrame::nsListControlFrame(ComputedStyle* aStyle,
nsPresContext* aPresContext)
: ScrollContainerFrame(aStyle, aPresContext, kClassID, false),
mNeedToReset(true),
mPostChildrenLoadedReset(false),
mMightNeedSecondPass(false),
mReflowWasInterrupted(false) {}
nsListControlFrame::~nsListControlFrame() = default;
Maybe<nscoord> nsListControlFrame::GetNaturalBaselineBOffset(
WritingMode aWM, BaselineSharingGroup aBaselineGroup,
BaselineExportContext) const {
// Unlike scroll frames which we inherit from, we don't export a baseline.
return Nothing{};
}
HTMLOptionElement* nsListControlFrame::GetCurrentOption() const {
return Select().GetCurrentOption();
}
bool nsListControlFrame::IsFocused() const {
return Select().State().HasState(ElementState::FOCUS);
}
void nsListControlFrame::InvalidateFocus() { InvalidateFrame(); }
NS_QUERYFRAME_HEAD(nsListControlFrame)
NS_QUERYFRAME_ENTRY(nsListControlFrame)
NS_QUERYFRAME_TAIL_INHERITING(ScrollContainerFrame)
#ifdef ACCESSIBILITY
a11y::AccType nsListControlFrame::AccessibleType() {
return a11y::eHTMLSelectListType;
}
#endif
// Return true if we found at least one <option> or non-empty <optgroup> label
// that has a frame. aResult will be the maximum BSize of those.
static bool GetMaxRowBSize(nsIFrame* aContainer, WritingMode aWM,
nscoord* aResult) {
bool found = false;
for (nsIFrame* child : aContainer->PrincipalChildList()) {
if (child->GetContent()->IsHTMLElement(nsGkAtoms::optgroup)) {
// An optgroup; drill through any scroll frame and recurse. |inner| might
// be null here though if |inner| is an anonymous leaf frame of some sort.
auto inner = child->GetContentInsertionFrame();
if (inner && GetMaxRowBSize(inner, aWM, aResult)) {
found = true;
}
} else {
// an option or optgroup label
bool isOptGroupLabel =
child->Style()->IsPseudoElement() &&
aContainer->GetContent()->IsHTMLElement(nsGkAtoms::optgroup);
nscoord childBSize = child->BSize(aWM);
// XXX bug 1499176: skip empty <optgroup> labels (zero bsize) for now
if (!isOptGroupLabel || childBSize > nscoord(0)) {
found = true;
*aResult = std::max(childBSize, *aResult);
}
}
}
return found;
}
//-----------------------------------------------------------------
// Main Reflow for ListBox/Dropdown
//-----------------------------------------------------------------
nscoord nsListControlFrame::CalcBSizeOfARow() {
// Calculate the block size in our writing mode of a single row in the
// listbox or dropdown list by using the tallest thing in the subtree,
// since there may be option groups in addition to option elements,
// either of which may be visible or invisible, may use different
// fonts, etc.
nscoord rowBSize(0);
if (GetContainSizeAxes().mBContained ||
!GetMaxRowBSize(GetContentInsertionFrame(), GetWritingMode(),
&rowBSize)) {
// We don't have any <option>s or <optgroup> labels with a frame.
// (Or we're size-contained in block axis, which has the same outcome for
// our sizing.)
float inflation = nsLayoutUtils::FontSizeInflationFor(this);
rowBSize = CalcFallbackRowBSize(inflation);
}
return rowBSize;
}
nscoord nsListControlFrame::IntrinsicISize(const IntrinsicSizeInput& aInput,
IntrinsicISizeType aType) {
// Always add scrollbar inline sizes to the intrinsic isize of the
// scrolled content. Combobox frames depend on this happening in the
// dropdown, and standalone listboxes are overflow:scroll so they need
// it too.
WritingMode wm = GetWritingMode();
nscoord result;
if (Maybe<nscoord> containISize = ContainIntrinsicISize()) {
result = *containISize;
} else {
result = GetScrolledFrame()->IntrinsicISize(aInput, aType);
}
LogicalMargin scrollbarSize(wm, GetDesiredScrollbarSizes());
result = NSCoordSaturatingAdd(result, scrollbarSize.IStartEnd(wm));
return result;
}
void nsListControlFrame::Reflow(nsPresContext* aPresContext,
ReflowOutput& aDesiredSize,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
NS_WARNING_ASSERTION(aReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE,
"Must have a computed inline size");
const bool hadPendingInterrupt = aPresContext->HasPendingInterrupt();
SchedulePaint();
MarkInReflow();
// Due to the fact that our intrinsic block size depends on the block
// sizes of our kids, we end up having to do two-pass reflow, in
// general -- the first pass to find the intrinsic block size and a
// second pass to reflow the scrollframe at that block size (which
// will size the scrollbars correctly, etc).
//
// Naturally, we want to avoid doing the second reflow as much as
// possible. We can skip it in the following cases (in all of which the first
// reflow is already happening at the right block size):
bool autoBSize = (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE);
Maybe<nscoord> containBSize = ContainIntrinsicBSize(NS_UNCONSTRAINEDSIZE);
bool usingContainBSize =
autoBSize && containBSize && *containBSize != NS_UNCONSTRAINEDSIZE;
mMightNeedSecondPass = [&] {
if (!autoBSize) {
// We're reflowing with a constrained computed block size -- just use that
// block size.
return false;
}
if (!IsSubtreeDirty() && !aReflowInput.ShouldReflowAllKids()) {
// We're not dirty and have no dirty kids and shouldn't be reflowing all
// kids. In this case, our cached max block size of a child is not going
// to change.
return false;
}
if (usingContainBSize) {
// We're size-contained in the block axis. In this case the size of a row
// doesn't depend on our children (it's the "fallback" size).
return false;
}
// We might need to do a second pass. If we do our first reflow using our
// cached max block size of a child, then compute the new max block size,
// and it's the same as the old one, we might still skip it (see the
// IsScrollbarUpdateSuppressed() check).
return true;
}();
ReflowInput state(aReflowInput);
int32_t length = GetNumberOfRows();
nscoord oldBSizeOfARow = BSizeOfARow();
if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && autoBSize) {
// When not doing an initial reflow, and when the block size is
// auto, start off with our computed block size set to what we'd
// expect our block size to be.
nscoord computedBSize = CalcIntrinsicBSize(oldBSizeOfARow, length);
computedBSize = state.ApplyMinMaxBSize(computedBSize);
state.SetComputedBSize(computedBSize);
}
if (usingContainBSize) {
state.SetComputedBSize(*containBSize);
}
ScrollContainerFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
mBSizeOfARow = CalcBSizeOfARow();
if (!mMightNeedSecondPass) {
NS_ASSERTION(
!autoBSize || usingContainBSize || BSizeOfARow() == oldBSizeOfARow,
"How did our BSize of a row change if nothing was dirty?");
NS_ASSERTION(!autoBSize || usingContainBSize ||
!HasAnyStateBits(NS_FRAME_FIRST_REFLOW),
"How do we not need a second pass during initial reflow at "
"auto BSize?");
if (!autoBSize || usingContainBSize) {
// Update our mNumDisplayRows based on our new row block size now
// that we know it. Note that if autoBSize and we landed in this
// code then we already set mNumDisplayRows in CalcIntrinsicBSize.
// Also note that we can't use BSizeOfARow() here because that
// just uses a cached value that we didn't compute.
nscoord rowBSize = CalcBSizeOfARow();
if (rowBSize == 0) {
// Just pick something
mNumDisplayRows = 1;
} else {
mNumDisplayRows = std::max(1, state.ComputedBSize() / rowBSize);
}
}
return;
}
mMightNeedSecondPass = false;
// Now see whether we need a second pass. If we do, our
// nsSelectsAreaFrame will have suppressed the scrollbar update.
if (mBSizeOfARow == oldBSizeOfARow) {
return;
}
// Gotta reflow again.
// XXXbz We're just changing the block size here; do we need to dirty
// ourselves or anything like that? We might need to, per the letter
// of the reflow protocol, but things seem to work fine without it...
// Is that just an implementation detail of ScrollContainerFrame that
// we're depending on?
ScrollContainerFrame::DidReflow(aPresContext, &state);
// Now compute the block size we want to have
nscoord computedBSize = CalcIntrinsicBSize(BSizeOfARow(), length);
computedBSize = state.ApplyMinMaxBSize(computedBSize);
state.SetComputedBSize(computedBSize);
// XXXbz to make the ascent really correct, we should add our
// mComputedPadding.top to it (and subtract it from descent). Need that
// because ScrollContainerFrame just adds in the border....
aStatus.Reset();
ScrollContainerFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
mReflowWasInterrupted |=
!hadPendingInterrupt && aPresContext->HasPendingInterrupt();
}
static uint32_t CountOptionsAndOptgroups(nsIFrame* aFrame) {
uint32_t count = 0;
for (nsIFrame* child : aFrame->PrincipalChildList()) {
nsIContent* content = child->GetContent();
if (content) {
if (content->IsHTMLElement(nsGkAtoms::option)) {
++count;
} else {
RefPtr<HTMLOptGroupElement> optgroup =
HTMLOptGroupElement::FromNode(content);
if (optgroup) {
nsAutoString label;
optgroup->GetLabel(label);
if (label.Length() > 0) {
++count;
}
count += CountOptionsAndOptgroups(child);
}
}
}
}
return count;
}
uint32_t nsListControlFrame::GetNumberOfRows() {
return ::CountOptionsAndOptgroups(GetContentInsertionFrame());
}
//---------------------------------------------------------
nsresult nsListControlFrame::HandleEvent(nsPresContext* aPresContext,
WidgetGUIEvent* aEvent,
nsEventStatus* aEventStatus) {
NS_ENSURE_ARG_POINTER(aEventStatus);
/*const char * desc[] = {"eMouseMove",
"NS_MOUSE_LEFT_BUTTON_UP",
"NS_MOUSE_LEFT_BUTTON_DOWN",
"<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>",
"NS_MOUSE_MIDDLE_BUTTON_UP",
"NS_MOUSE_MIDDLE_BUTTON_DOWN",
"<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>",
"NS_MOUSE_RIGHT_BUTTON_UP",
"NS_MOUSE_RIGHT_BUTTON_DOWN",
"eMouseOver",
"eMouseOut",
"NS_MOUSE_LEFT_DOUBLECLICK",
"NS_MOUSE_MIDDLE_DOUBLECLICK",
"NS_MOUSE_RIGHT_DOUBLECLICK",
"NS_MOUSE_LEFT_CLICK",
"NS_MOUSE_MIDDLE_CLICK",
"NS_MOUSE_RIGHT_CLICK"};
int inx = aEvent->mMessage - eMouseEventFirst;
if (inx >= 0 && inx <= (NS_MOUSE_RIGHT_CLICK - eMouseEventFirst)) {
printf("Mouse in ListFrame %s [%d]\n", desc[inx], aEvent->mMessage);
} else {
printf("Mouse in ListFrame <UNKNOWN> [%d]\n", aEvent->mMessage);
}*/
if (nsEventStatus_eConsumeNoDefault == *aEventStatus) {
return NS_OK;
}
// disabled state affects how we're selected, but we don't want to go through
// ScrollContainerFrame if we're disabled.
if (IsContentDisabled()) {
return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
}
return ScrollContainerFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
}
HTMLSelectElement& nsListControlFrame::Select() const {
return *static_cast<HTMLSelectElement*>(GetContent());
}
//---------------------------------------------------------
void nsListControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
nsIFrame* aPrevInFlow) {
ScrollContainerFrame::Init(aContent, aParent, aPrevInFlow);
}
dom::HTMLOptionElement* nsListControlFrame::GetOption(uint32_t aIndex) const {
return Select().Item(aIndex);
}
void nsListControlFrame::OnSelectionReset() {
mPostChildrenLoadedReset = true;
InvalidateFocus();
}
void nsListControlFrame::ElementStateChanged(ElementState aStates) {
if (aStates.HasState(ElementState::FOCUS)) {
InvalidateFocus();
}
}
void nsListControlFrame::GetOptionText(uint32_t aIndex, nsAString& aStr) {
aStr.Truncate();
if (dom::HTMLOptionElement* optionElement = GetOption(aIndex)) {
optionElement->GetRenderedLabel(aStr);
}
}
void nsListControlFrame::OptionsAdded() {
// Make sure we scroll to the selected option as needed
mNeedToReset = true;
if (Select().IsDoneAddingChildren()) {
mPostChildrenLoadedReset = true;
}
}
class AsyncReset final : public Runnable {
public:
AsyncReset(HTMLSelectElement& aElement, bool aScroll)
: Runnable("AsyncReset"), mElement(&aElement), mScroll(aScroll) {}
MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
MOZ_KnownLive(mElement)->ResetListBoxSelection(mScroll);
return NS_OK;
}
private:
const RefPtr<HTMLSelectElement> mElement;
const bool mScroll;
};
bool nsListControlFrame::ReflowFinished() {
if (mNeedToReset && !mReflowWasInterrupted) {
mNeedToReset = false;
// Suppress scrolling to the selected element if we restored scroll
// history state AND the list contents have not changed since we loaded
// all the children AND nothing else forced us to scroll by calling
// ResetList(true). The latter two conditions are folded into
// mPostChildrenLoadedReset.
//
// The idea is that we want scroll history restoration to trump ResetList
// scrolling to the selected element, when the ResetList was probably only
// caused by content loading normally.
const bool scroll = !DidHistoryRestore() || mPostChildrenLoadedReset;
nsContentUtils::AddScriptRunner(
MakeAndAddRef<AsyncReset>(Select(), scroll));
}
mReflowWasInterrupted = false;
return ScrollContainerFrame::ReflowFinished();
}
#ifdef DEBUG_FRAME_DUMP
nsresult nsListControlFrame::GetFrameName(nsAString& aResult) const {
return MakeFrameName(u"ListControl"_ns, aResult);
}
#endif
nscoord nsListControlFrame::GetBSizeOfARow() { return BSizeOfARow(); }
nscoord nsListControlFrame::CalcFallbackRowBSize(float aFontSizeInflation) {
RefPtr<nsFontMetrics> fontMet =
nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation);
return fontMet->MaxHeight();
}
nscoord nsListControlFrame::CalcIntrinsicBSize(nscoord aBSizeOfARow,
int32_t aNumberOfOptions) {
if (Style()->StyleUIReset()->mFieldSizing == StyleFieldSizing::Content) {
int32_t length = GetNumberOfRows();
return length * aBSizeOfARow;
}
mNumDisplayRows = Select().Size();
if (mNumDisplayRows < 1) {
mNumDisplayRows = 4;
}
return mNumDisplayRows * aBSizeOfARow;
}
void nsListControlFrame::ScrollToFrame(dom::HTMLOptionElement& aOptElement) {
// otherwise we find the content's frame and scroll to it
if (nsIFrame* childFrame = aOptElement.GetPrimaryFrame()) {
RefPtr<mozilla::PresShell> presShell = PresShell();
presShell->ScrollFrameIntoView(childFrame, Nothing(), AxisScrollParams(),
AxisScrollParams(),
ScrollFlags::ScrollOverflowHidden |
ScrollFlags::ScrollFirstAncestorOnly);
}
}