blob: 16f5d4398749a141cb2a93078ebc6ed8c5b3be69 [file] [log] [blame]
/*
* This file is part of the select element layoutObject in WebCore.
*
* Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
* Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc.
* All rights reserved.
* (C) 2009 Torch Mobile Inc. All rights reserved.
* (http://www.torchmobile.com/)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
#include "core/layout/LayoutMenuList.h"
#include <math.h>
#include "core/dom/AXObjectCache.h"
#include "core/dom/NodeComputedStyle.h"
#include "core/frame/LocalFrameView.h"
#include "core/html/forms/HTMLOptionElement.h"
#include "core/html/forms/HTMLSelectElement.h"
#include "core/layout/LayoutText.h"
#include "core/layout/LayoutTheme.h"
#include "platform/text/PlatformLocale.h"
namespace blink {
LayoutMenuList::LayoutMenuList(Element* element)
: LayoutFlexibleBox(element),
button_text_(nullptr),
inner_block_(nullptr),
is_empty_(false),
has_updated_active_option_(false),
inner_block_height_(LayoutUnit()),
options_width_(0),
last_active_index_(-1) {
DCHECK(IsHTMLSelectElement(element));
}
LayoutMenuList::~LayoutMenuList() = default;
// FIXME: Instead of this hack we should add a ShadowRoot to <select> with no
// insertion point to prevent children from rendering.
bool LayoutMenuList::IsChildAllowed(LayoutObject* object,
const ComputedStyle&) const {
return object->IsAnonymous() && !object->IsLayoutFullScreen();
}
void LayoutMenuList::CreateInnerBlock() {
if (inner_block_) {
DCHECK_EQ(FirstChild(), inner_block_);
DCHECK(!inner_block_->NextSibling());
return;
}
// Create an anonymous block.
DCHECK(!FirstChild());
inner_block_ = CreateAnonymousBlock();
button_text_ = LayoutText::CreateEmptyAnonymous(GetDocument());
// We need to set the text explicitly though it was specified in the
// constructor because LayoutText doesn't refer to the text
// specified in the constructor in a case of re-transforming.
button_text_->SetStyle(MutableStyle());
inner_block_->AddChild(button_text_);
AdjustInnerStyle();
LayoutFlexibleBox::AddChild(inner_block_);
}
void LayoutMenuList::AdjustInnerStyle() {
ComputedStyle& inner_style = inner_block_->MutableStyleRef();
inner_style.SetFlexGrow(1);
inner_style.SetFlexShrink(1);
// min-width: 0; is needed for correct shrinking.
inner_style.SetMinWidth(Length(0, kFixed));
// Use margin:auto instead of align-items:center to get safe centering, i.e.
// when the content overflows, treat it the same as align-items: flex-start.
// But we only do that for the cases where html.css would otherwise use
// center.
if (Style()->AlignItemsPosition() == ItemPosition::kCenter) {
inner_style.SetMarginTop(Length());
inner_style.SetMarginBottom(Length());
inner_style.SetAlignSelfPosition(ItemPosition::kFlexStart);
}
Length padding_start = Length(
LayoutTheme::GetTheme().PopupInternalPaddingStart(StyleRef()), kFixed);
Length padding_end =
Length(LayoutTheme::GetTheme().PopupInternalPaddingEnd(
GetFrameView()->GetChromeClient(), StyleRef()),
kFixed);
inner_style.SetPaddingLeft(StyleRef().Direction() == TextDirection::kLtr
? padding_start
: padding_end);
inner_style.SetPaddingRight(StyleRef().Direction() == TextDirection::kLtr
? padding_end
: padding_start);
inner_style.SetPaddingTop(Length(
LayoutTheme::GetTheme().PopupInternalPaddingTop(StyleRef()), kFixed));
inner_style.SetPaddingBottom(Length(
LayoutTheme::GetTheme().PopupInternalPaddingBottom(StyleRef()), kFixed));
if (option_style_) {
if ((option_style_->Direction() != inner_style.Direction() ||
option_style_->GetUnicodeBidi() != inner_style.GetUnicodeBidi()))
inner_block_->SetNeedsLayoutAndPrefWidthsRecalcAndFullPaintInvalidation(
LayoutInvalidationReason::kStyleChange);
inner_style.SetTextAlign(Style()->IsLeftToRightDirection()
? ETextAlign::kLeft
: ETextAlign::kRight);
inner_style.SetDirection(option_style_->Direction());
inner_style.SetUnicodeBidi(option_style_->GetUnicodeBidi());
}
// LayoutMenuList::ControlClipRect() depends on inner_block_->ContentsSize().
SetNeedsPaintPropertyUpdate();
}
HTMLSelectElement* LayoutMenuList::SelectElement() const {
return ToHTMLSelectElement(GetNode());
}
void LayoutMenuList::AddChild(LayoutObject* new_child,
LayoutObject* before_child) {
inner_block_->AddChild(new_child, before_child);
DCHECK_EQ(inner_block_, FirstChild());
if (AXObjectCache* cache = GetDocument().ExistingAXObjectCache())
cache->ChildrenChanged(this);
// LayoutMenuList::ControlClipRect() depends on inner_block_->ContentsSize().
SetNeedsPaintPropertyUpdate();
}
void LayoutMenuList::RemoveChild(LayoutObject* old_child) {
if (old_child == inner_block_ || !inner_block_) {
LayoutFlexibleBox::RemoveChild(old_child);
inner_block_ = nullptr;
} else {
inner_block_->RemoveChild(old_child);
}
}
void LayoutMenuList::StyleDidChange(StyleDifference diff,
const ComputedStyle* old_style) {
LayoutBlock::StyleDidChange(diff, old_style);
if (!inner_block_)
CreateInnerBlock();
button_text_->SetStyle(MutableStyle());
AdjustInnerStyle();
UpdateInnerBlockHeight();
}
void LayoutMenuList::UpdateInnerBlockHeight() {
const SimpleFontData* font_data = Style()->GetFont().PrimaryFont();
DCHECK(font_data);
inner_block_height_ = (font_data ? font_data->GetFontMetrics().Height() : 0) +
inner_block_->BorderAndPaddingHeight();
}
void LayoutMenuList::UpdateOptionsWidth() const {
float max_option_width = 0;
for (auto* const option : SelectElement()->GetOptionList()) {
String text = option->TextIndentedToRespectGroupLabel();
const ComputedStyle* item_style =
option->GetComputedStyle() ? option->GetComputedStyle() : Style();
ApplyTextTransform(item_style, text, ' ');
// We apply SELECT's style, not OPTION's style because m_optionsWidth is
// used to determine intrinsic width of the menulist box.
TextRun text_run = ConstructTextRun(Style()->GetFont(), text, *Style());
max_option_width =
std::max(max_option_width, Style()->GetFont().Width(text_run));
}
options_width_ = static_cast<int>(ceilf(max_option_width));
}
void LayoutMenuList::UpdateFromElement() {
HTMLSelectElement* select = SelectElement();
HTMLOptionElement* option = select->OptionToBeShown();
String text = g_empty_string;
option_style_ = nullptr;
if (select->IsMultiple()) {
unsigned selected_count = 0;
HTMLOptionElement* selected_option_element = nullptr;
for (auto* const option : select->GetOptionList()) {
if (option->Selected()) {
if (++selected_count == 1)
selected_option_element = option;
}
}
if (selected_count == 1) {
text = selected_option_element->TextIndentedToRespectGroupLabel();
option_style_ = selected_option_element->MutableComputedStyle();
} else {
Locale& locale = select->GetLocale();
String localized_number_string =
locale.ConvertToLocalizedNumber(String::Number(selected_count));
text = locale.QueryString(WebLocalizedString::kSelectMenuListText,
localized_number_string);
DCHECK(!option_style_);
}
} else {
if (option) {
text = option->TextIndentedToRespectGroupLabel();
option_style_ = option->MutableComputedStyle();
}
}
SetText(text.StripWhiteSpace());
DidUpdateActiveOption(option);
}
void LayoutMenuList::SetText(const String& s) {
if (s.IsEmpty()) {
// FIXME: This is a hack. We need the select to have the same baseline
// positioning as any surrounding text. Wihtout any content, we align the
// bottom of the select to the bottom of the text. With content (In this
// case the faked " ") we correctly align the middle of the select to the
// middle of the text. It should be possible to remove this, just set
// s.impl() into the text and have things align correctly...
// crbug.com/485982
is_empty_ = true;
button_text_->SetText(StringImpl::Create(" ", 1), true);
} else {
is_empty_ = false;
button_text_->SetText(s.Impl(), true);
}
AdjustInnerStyle();
}
String LayoutMenuList::GetText() const {
return button_text_ && !is_empty_ ? button_text_->GetText() : String();
}
LayoutRect LayoutMenuList::ControlClipRect(
const LayoutPoint& additional_offset) const {
// Clip to the intersection of the content box and the content box for the
// inner box. This will leave room for the arrows which sit in the inner box
// padding, and if the inner box ever spills out of the outer box, that will
// get clipped too.
LayoutRect outer_box = ContentBoxRect();
outer_box.MoveBy(additional_offset);
LayoutRect inner_box(
additional_offset + inner_block_->Location() +
LayoutSize(inner_block_->PaddingLeft(), inner_block_->PaddingTop()),
inner_block_->ContentSize());
return Intersection(outer_box, inner_box);
}
void LayoutMenuList::ComputeIntrinsicLogicalWidths(
LayoutUnit& min_logical_width,
LayoutUnit& max_logical_width) const {
UpdateOptionsWidth();
max_logical_width =
std::max(options_width_,
LayoutTheme::GetTheme().MinimumMenuListSize(StyleRef())) +
inner_block_->PaddingLeft() + inner_block_->PaddingRight();
if (!Style()->Width().IsPercentOrCalc())
min_logical_width = max_logical_width;
else
min_logical_width = LayoutUnit();
}
void LayoutMenuList::ComputeLogicalHeight(
LayoutUnit logical_height,
LayoutUnit logical_top,
LogicalExtentComputedValues& computed_values) const {
if (Style()->HasAppearance())
logical_height = inner_block_height_ + BorderAndPaddingHeight();
LayoutBox::ComputeLogicalHeight(logical_height, logical_top, computed_values);
}
void LayoutMenuList::DidSelectOption(HTMLOptionElement* option) {
DidUpdateActiveOption(option);
}
void LayoutMenuList::DidUpdateActiveOption(HTMLOptionElement* option) {
if (!GetDocument().ExistingAXObjectCache())
return;
int option_index = option ? option->index() : -1;
if (last_active_index_ == option_index)
return;
last_active_index_ = option_index;
// We skip sending accessiblity notifications for the very first option,
// otherwise we get extra focus and select events that are undesired.
if (!has_updated_active_option_) {
has_updated_active_option_ = true;
return;
}
GetDocument().ExistingAXObjectCache()->HandleUpdateActiveMenuOption(
this, option_index);
}
LayoutUnit LayoutMenuList::ClientPaddingLeft() const {
return PaddingLeft() + inner_block_->PaddingLeft();
}
LayoutUnit LayoutMenuList::ClientPaddingRight() const {
return PaddingRight() + inner_block_->PaddingRight();
}
} // namespace blink