1/*
2 * Copyright (C) 2006-2018 Apple Inc. All rights reserved.
3 * 2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/)
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of Apple Inc. ("Apple") nor the names of
15 * its contributors may be used to endorse or promote products derived
16 * from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30#include "config.h"
31#include "RenderListBox.h"
32
33#include "AXObjectCache.h"
34#include "CSSFontSelector.h"
35#include "DeprecatedGlobalSettings.h"
36#include "Document.h"
37#include "DocumentEventQueue.h"
38#include "EventHandler.h"
39#include "FocusController.h"
40#include "Frame.h"
41#include "FrameSelection.h"
42#include "FrameView.h"
43#include "GraphicsContext.h"
44#include "HTMLNames.h"
45#include "HTMLOptionElement.h"
46#include "HTMLOptGroupElement.h"
47#include "HTMLSelectElement.h"
48#include "HitTestResult.h"
49#include "NodeRenderStyle.h"
50#include "Page.h"
51#include "PaintInfo.h"
52#include "RenderLayer.h"
53#include "RenderLayoutState.h"
54#include "RenderScrollbar.h"
55#include "RenderText.h"
56#include "RenderTheme.h"
57#include "RenderView.h"
58#include "ScrollAnimator.h"
59#include "Scrollbar.h"
60#include "ScrollbarTheme.h"
61#include "Settings.h"
62#include "SpatialNavigation.h"
63#include "StyleResolver.h"
64#include "StyleTreeResolver.h"
65#include "WheelEventTestTrigger.h"
66#include <math.h>
67#include <wtf/IsoMallocInlines.h>
68#include <wtf/StackStats.h>
69
70namespace WebCore {
71
72using namespace HTMLNames;
73
74WTF_MAKE_ISO_ALLOCATED_IMPL(RenderListBox);
75
76const int rowSpacing = 1;
77
78const int optionsSpacingHorizontal = 2;
79
80// The minSize constant was originally defined to render scrollbars correctly.
81// This might vary for different platforms.
82const int minSize = 4;
83
84// Default size when the multiple attribute is present but size attribute is absent.
85const int defaultSize = 4;
86
87// FIXME: This hardcoded baselineAdjustment is what we used to do for the old
88// widget, but I'm not sure this is right for the new control.
89const int baselineAdjustment = 7;
90
91RenderListBox::RenderListBox(HTMLSelectElement& element, RenderStyle&& style)
92 : RenderBlockFlow(element, WTFMove(style))
93 , m_optionsChanged(true)
94 , m_scrollToRevealSelectionAfterLayout(false)
95 , m_inAutoscroll(false)
96 , m_optionsWidth(0)
97 , m_indexOffset(0)
98{
99 view().frameView().addScrollableArea(this);
100}
101
102RenderListBox::~RenderListBox()
103{
104 // Do not add any code here. Add it to willBeDestroyed() instead.
105}
106
107void RenderListBox::willBeDestroyed()
108{
109 setHasVerticalScrollbar(false);
110 view().frameView().removeScrollableArea(this);
111 RenderBlockFlow::willBeDestroyed();
112}
113
114HTMLSelectElement& RenderListBox::selectElement() const
115{
116 return downcast<HTMLSelectElement>(nodeForNonAnonymous());
117}
118
119static FontCascade bolder(Document& document, const FontCascade& font)
120{
121 auto description = font.fontDescription();
122 description.setWeight(description.bolderWeight());
123 auto result = FontCascade { WTFMove(description), font.letterSpacing(), font.wordSpacing() };
124 result.update(&document.fontSelector());
125 return result;
126}
127
128void RenderListBox::updateFromElement()
129{
130 if (m_optionsChanged) {
131 float width = 0;
132 auto& normalFont = style().fontCascade();
133 Optional<FontCascade> boldFont;
134 for (auto* element : selectElement().listItems()) {
135 String text;
136 WTF::Function<const FontCascade&()> selectFont = [&normalFont] () -> const FontCascade& {
137 return normalFont;
138 };
139 if (is<HTMLOptionElement>(*element))
140 text = downcast<HTMLOptionElement>(*element).textIndentedToRespectGroupLabel();
141 else if (is<HTMLOptGroupElement>(*element)) {
142 text = downcast<HTMLOptGroupElement>(*element).groupLabelText();
143 selectFont = [this, &normalFont, &boldFont] () -> const FontCascade& {
144 if (!boldFont)
145 boldFont = bolder(document(), normalFont);
146 return boldFont.value();
147 };
148 }
149 if (text.isEmpty())
150 continue;
151 text = applyTextTransform(style(), text, ' ');
152 auto textRun = constructTextRun(text, style(), AllowTrailingExpansion);
153 width = std::max(width, selectFont().width(textRun));
154 }
155 // FIXME: Is ceiling right here, or should we be doing some kind of rounding instead?
156 m_optionsWidth = static_cast<int>(std::ceil(width));
157 m_optionsChanged = false;
158
159 setHasVerticalScrollbar(true);
160
161 computeFirstIndexesVisibleInPaddingTopBottomAreas();
162
163 setNeedsLayoutAndPrefWidthsRecalc();
164 }
165}
166
167void RenderListBox::selectionChanged()
168{
169 repaint();
170 if (!m_inAutoscroll) {
171 if (m_optionsChanged || needsLayout())
172 m_scrollToRevealSelectionAfterLayout = true;
173 else
174 scrollToRevealSelection();
175 }
176
177 if (AXObjectCache* cache = document().existingAXObjectCache())
178 cache->deferSelectedChildrenChangedIfNeeded(selectElement());
179}
180
181void RenderListBox::layout()
182{
183 StackStats::LayoutCheckPoint layoutCheckPoint;
184 RenderBlockFlow::layout();
185
186 if (m_vBar) {
187 bool enabled = numVisibleItems() < numItems();
188 m_vBar->setEnabled(enabled);
189 m_vBar->setSteps(1, std::max(1, numVisibleItems() - 1), itemHeight());
190 m_vBar->setProportion(numVisibleItems(), numItems());
191 if (!enabled) {
192 scrollToOffsetWithoutAnimation(VerticalScrollbar, 0);
193 m_indexOffset = 0;
194 }
195 }
196
197 if (m_scrollToRevealSelectionAfterLayout) {
198 LayoutStateDisabler layoutStateDisabler(view().frameView().layoutContext());
199 scrollToRevealSelection();
200 }
201}
202
203void RenderListBox::scrollToRevealSelection()
204{
205 m_scrollToRevealSelectionAfterLayout = false;
206
207 int firstIndex = selectElement().activeSelectionStartListIndex();
208 if (firstIndex >= 0 && !listIndexIsVisible(selectElement().activeSelectionEndListIndex()))
209 scrollToRevealElementAtListIndex(firstIndex);
210}
211
212void RenderListBox::computeIntrinsicLogicalWidths(LayoutUnit& minLogicalWidth, LayoutUnit& maxLogicalWidth) const
213{
214 maxLogicalWidth = m_optionsWidth + 2 * optionsSpacingHorizontal;
215 if (m_vBar)
216 maxLogicalWidth += m_vBar->width();
217 if (!style().width().isPercentOrCalculated())
218 minLogicalWidth = maxLogicalWidth;
219}
220
221void RenderListBox::computePreferredLogicalWidths()
222{
223 // Nested style recal do not fire post recal callbacks. see webkit.org/b/153767
224 ASSERT(!m_optionsChanged || Style::postResolutionCallbacksAreSuspended());
225
226 m_minPreferredLogicalWidth = 0;
227 m_maxPreferredLogicalWidth = 0;
228
229 if (style().width().isFixed() && style().width().value() > 0)
230 m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(style().width().value());
231 else
232 computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth);
233
234 if (style().minWidth().isFixed() && style().minWidth().value() > 0) {
235 m_maxPreferredLogicalWidth = std::max(m_maxPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().minWidth().value()));
236 m_minPreferredLogicalWidth = std::max(m_minPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().minWidth().value()));
237 }
238
239 if (style().maxWidth().isFixed()) {
240 m_maxPreferredLogicalWidth = std::min(m_maxPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().maxWidth().value()));
241 m_minPreferredLogicalWidth = std::min(m_minPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().maxWidth().value()));
242 }
243
244 LayoutUnit toAdd = horizontalBorderAndPaddingExtent();
245 m_minPreferredLogicalWidth += toAdd;
246 m_maxPreferredLogicalWidth += toAdd;
247
248 setPreferredLogicalWidthsDirty(false);
249}
250
251int RenderListBox::size() const
252{
253 int specifiedSize = selectElement().size();
254 if (specifiedSize > 1)
255 return std::max(minSize, specifiedSize);
256
257 return defaultSize;
258}
259
260int RenderListBox::numVisibleItems(ConsiderPadding considerPadding) const
261{
262 // Only count fully visible rows. But don't return 0 even if only part of a row shows.
263 int visibleItemsExcludingPadding = std::max<int>(1, (contentHeight() + rowSpacing) / itemHeight());
264 if (considerPadding == ConsiderPadding::No)
265 return visibleItemsExcludingPadding;
266
267 return numberOfVisibleItemsInPaddingTop() + visibleItemsExcludingPadding + numberOfVisibleItemsInPaddingBottom();
268}
269
270int RenderListBox::numItems() const
271{
272 return selectElement().listItems().size();
273}
274
275LayoutUnit RenderListBox::listHeight() const
276{
277 return itemHeight() * numItems() - rowSpacing;
278}
279
280RenderBox::LogicalExtentComputedValues RenderListBox::computeLogicalHeight(LayoutUnit, LayoutUnit logicalTop) const
281{
282 LayoutUnit height = itemHeight() * size() - rowSpacing;
283 cacheIntrinsicContentLogicalHeightForFlexItem(height);
284 height += verticalBorderAndPaddingExtent();
285 return RenderBox::computeLogicalHeight(height, logicalTop);
286}
287
288int RenderListBox::baselinePosition(FontBaseline baselineType, bool firstLine, LineDirectionMode lineDirection, LinePositionMode linePositionMode) const
289{
290 return RenderBox::baselinePosition(baselineType, firstLine, lineDirection, linePositionMode) - baselineAdjustment;
291}
292
293LayoutRect RenderListBox::itemBoundingBoxRect(const LayoutPoint& additionalOffset, int index)
294{
295 LayoutUnit x = additionalOffset.x() + borderLeft() + paddingLeft();
296 if (shouldPlaceBlockDirectionScrollbarOnLeft() && m_vBar)
297 x += m_vBar->occupiedWidth();
298 LayoutUnit y = additionalOffset.y() + borderTop() + paddingTop() + itemHeight() * (index - m_indexOffset);
299 return LayoutRect(x, y, contentWidth(), itemHeight());
300}
301
302void RenderListBox::paintItem(PaintInfo& paintInfo, const LayoutPoint& paintOffset, const PaintFunction& paintFunction)
303{
304 int listItemsSize = numItems();
305 int firstVisibleItem = m_indexOfFirstVisibleItemInsidePaddingTopArea.valueOr(m_indexOffset);
306 int endIndex = firstVisibleItem + numVisibleItems(ConsiderPadding::Yes);
307 for (int i = firstVisibleItem; i < listItemsSize && i < endIndex; ++i)
308 paintFunction(paintInfo, paintOffset, i);
309}
310
311void RenderListBox::paintObject(PaintInfo& paintInfo, const LayoutPoint& paintOffset)
312{
313 if (style().visibility() != Visibility::Visible)
314 return;
315
316 if (paintInfo.phase == PaintPhase::Foreground) {
317 paintItem(paintInfo, paintOffset, [this](PaintInfo& paintInfo, const LayoutPoint& paintOffset, int listItemIndex) {
318 paintItemForeground(paintInfo, paintOffset, listItemIndex);
319 });
320 }
321
322 // Paint the children.
323 RenderBlockFlow::paintObject(paintInfo, paintOffset);
324
325 switch (paintInfo.phase) {
326 // Depending on whether we have overlay scrollbars they
327 // get rendered in the foreground or background phases
328 case PaintPhase::Foreground:
329 if (m_vBar->isOverlayScrollbar())
330 paintScrollbar(paintInfo, paintOffset);
331 break;
332 case PaintPhase::BlockBackground:
333 if (!m_vBar->isOverlayScrollbar())
334 paintScrollbar(paintInfo, paintOffset);
335 break;
336 case PaintPhase::ChildBlockBackground:
337 case PaintPhase::ChildBlockBackgrounds: {
338 paintItem(paintInfo, paintOffset, [this](PaintInfo& paintInfo, const LayoutPoint& paintOffset, int listItemIndex) {
339 paintItemBackground(paintInfo, paintOffset, listItemIndex);
340 });
341 break;
342 }
343 default:
344 break;
345 }
346}
347
348void RenderListBox::addFocusRingRects(Vector<LayoutRect>& rects, const LayoutPoint& additionalOffset, const RenderLayerModelObject* paintContainer)
349{
350 if (!selectElement().allowsNonContiguousSelection())
351 return RenderBlockFlow::addFocusRingRects(rects, additionalOffset, paintContainer);
352
353 // Focus the last selected item.
354 int selectedItem = selectElement().activeSelectionEndListIndex();
355 if (selectedItem >= 0) {
356 rects.append(snappedIntRect(itemBoundingBoxRect(additionalOffset, selectedItem)));
357 return;
358 }
359
360 // No selected items, find the first non-disabled item.
361 int size = numItems();
362 const Vector<HTMLElement*>& listItems = selectElement().listItems();
363 for (int i = 0; i < size; ++i) {
364 HTMLElement* element = listItems[i];
365 if (is<HTMLOptionElement>(*element) && !element->isDisabledFormControl()) {
366 selectElement().setActiveSelectionEndIndex(i);
367 rects.append(itemBoundingBoxRect(additionalOffset, i));
368 return;
369 }
370 }
371}
372
373void RenderListBox::paintScrollbar(PaintInfo& paintInfo, const LayoutPoint& paintOffset)
374{
375 if (!m_vBar)
376 return;
377
378 LayoutUnit left = paintOffset.x() + (shouldPlaceBlockDirectionScrollbarOnLeft() ? borderLeft() : width() - borderRight() - m_vBar->width());
379 LayoutUnit top = paintOffset.y() + borderTop();
380 LayoutUnit width = m_vBar->width();
381 LayoutUnit height = this->height() - (borderTop() + borderBottom());
382 IntRect scrollRect = snappedIntRect(left, top, width, height);
383 m_vBar->setFrameRect(scrollRect);
384 m_vBar->paint(paintInfo.context(), snappedIntRect(paintInfo.rect));
385}
386
387static LayoutSize itemOffsetForAlignment(TextRun textRun, const RenderStyle* itemStyle, FontCascade itemFont, LayoutRect itemBoudingBox)
388{
389 TextAlignMode actualAlignment = itemStyle->textAlign();
390 // FIXME: Firefox doesn't respect TextAlignMode::Justify. Should we?
391 // FIXME: Handle TextAlignMode::End here
392 if (actualAlignment == TextAlignMode::Start || actualAlignment == TextAlignMode::Justify)
393 actualAlignment = itemStyle->isLeftToRightDirection() ? TextAlignMode::Left : TextAlignMode::Right;
394
395 LayoutSize offset = LayoutSize(0, itemFont.fontMetrics().ascent());
396 if (actualAlignment == TextAlignMode::Right || actualAlignment == TextAlignMode::WebKitRight) {
397 float textWidth = itemFont.width(textRun);
398 offset.setWidth(itemBoudingBox.width() - textWidth - optionsSpacingHorizontal);
399 } else if (actualAlignment == TextAlignMode::Center || actualAlignment == TextAlignMode::WebKitCenter) {
400 float textWidth = itemFont.width(textRun);
401 offset.setWidth((itemBoudingBox.width() - textWidth) / 2);
402 } else
403 offset.setWidth(optionsSpacingHorizontal);
404 return offset;
405}
406
407void RenderListBox::paintItemForeground(PaintInfo& paintInfo, const LayoutPoint& paintOffset, int listIndex)
408{
409 const Vector<HTMLElement*>& listItems = selectElement().listItems();
410 HTMLElement* listItemElement = listItems[listIndex];
411
412 auto& itemStyle = *listItemElement->computedStyle();
413
414 if (itemStyle.visibility() == Visibility::Hidden)
415 return;
416
417 String itemText;
418 bool isOptionElement = is<HTMLOptionElement>(*listItemElement);
419 if (isOptionElement)
420 itemText = downcast<HTMLOptionElement>(*listItemElement).textIndentedToRespectGroupLabel();
421 else if (is<HTMLOptGroupElement>(*listItemElement))
422 itemText = downcast<HTMLOptGroupElement>(*listItemElement).groupLabelText();
423 itemText = applyTextTransform(style(), itemText, ' ');
424
425 if (itemText.isNull())
426 return;
427
428 Color textColor = itemStyle.visitedDependentColorWithColorFilter(CSSPropertyColor);
429 if (isOptionElement && downcast<HTMLOptionElement>(*listItemElement).selected()) {
430 if (frame().selection().isFocusedAndActive() && document().focusedElement() == &selectElement())
431 textColor = theme().activeListBoxSelectionForegroundColor(styleColorOptions());
432 // Honor the foreground color for disabled items
433 else if (!listItemElement->isDisabledFormControl() && !selectElement().isDisabledFormControl())
434 textColor = theme().inactiveListBoxSelectionForegroundColor(styleColorOptions());
435 }
436
437 paintInfo.context().setFillColor(textColor);
438
439 TextRun textRun(itemText, 0, 0, AllowTrailingExpansion, itemStyle.direction(), isOverride(itemStyle.unicodeBidi()), true);
440 FontCascade itemFont = style().fontCascade();
441 LayoutRect r = itemBoundingBoxRect(paintOffset, listIndex);
442 r.move(itemOffsetForAlignment(textRun, &itemStyle, itemFont, r));
443
444 if (is<HTMLOptGroupElement>(*listItemElement)) {
445 auto description = itemFont.fontDescription();
446 description.setWeight(description.bolderWeight());
447 itemFont = FontCascade(WTFMove(description), itemFont.letterSpacing(), itemFont.wordSpacing());
448 itemFont.update(&document().fontSelector());
449 }
450
451 // Draw the item text
452 paintInfo.context().drawBidiText(itemFont, textRun, roundedIntPoint(r.location()));
453}
454
455void RenderListBox::paintItemBackground(PaintInfo& paintInfo, const LayoutPoint& paintOffset, int listIndex)
456{
457 const Vector<HTMLElement*>& listItems = selectElement().listItems();
458 HTMLElement* listItemElement = listItems[listIndex];
459 auto& itemStyle = *listItemElement->computedStyle();
460
461 Color backColor;
462 if (is<HTMLOptionElement>(*listItemElement) && downcast<HTMLOptionElement>(*listItemElement).selected()) {
463 if (frame().selection().isFocusedAndActive() && document().focusedElement() == &selectElement())
464 backColor = theme().activeListBoxSelectionBackgroundColor(styleColorOptions());
465 else
466 backColor = theme().inactiveListBoxSelectionBackgroundColor(styleColorOptions());
467 } else
468 backColor = itemStyle.visitedDependentColorWithColorFilter(CSSPropertyBackgroundColor);
469
470 // Draw the background for this list box item
471 if (itemStyle.visibility() == Visibility::Hidden)
472 return;
473
474 LayoutRect itemRect = itemBoundingBoxRect(paintOffset, listIndex);
475 itemRect.intersect(controlClipRect(paintOffset));
476 paintInfo.context().fillRect(snappedIntRect(itemRect), backColor);
477}
478
479bool RenderListBox::isPointInOverflowControl(HitTestResult& result, const LayoutPoint& locationInContainer, const LayoutPoint& accumulatedOffset)
480{
481 if (!m_vBar || !m_vBar->shouldParticipateInHitTesting())
482 return false;
483
484 LayoutUnit x = accumulatedOffset.x() + (shouldPlaceBlockDirectionScrollbarOnLeft() ? borderLeft() : width() - borderRight() - m_vBar->width());
485 LayoutUnit y = accumulatedOffset.y() + borderTop();
486 LayoutUnit width = m_vBar->width();
487 LayoutUnit height = this->height() - borderTop() - borderBottom();
488 LayoutRect vertRect(x, y, width, height);
489
490 if (!vertRect.contains(locationInContainer))
491 return false;
492
493 result.setScrollbar(m_vBar.get());
494 return true;
495}
496
497int RenderListBox::listIndexAtOffset(const LayoutSize& offset)
498{
499 if (!numItems())
500 return -1;
501
502 if (offset.height() < borderTop() || offset.height() > height() - borderBottom())
503 return -1;
504
505 int scrollbarWidth = m_vBar ? m_vBar->width() : 0;
506 if (shouldPlaceBlockDirectionScrollbarOnLeft() && (offset.width() < borderLeft() + paddingLeft() + scrollbarWidth || offset.width() > width() - borderRight() - paddingRight()))
507 return -1;
508 if (!shouldPlaceBlockDirectionScrollbarOnLeft() && (offset.width() < borderLeft() + paddingLeft() || offset.width() > width() - borderRight() - paddingRight() - scrollbarWidth))
509 return -1;
510
511 int newOffset = (offset.height() - borderTop() - paddingTop()) / itemHeight() + m_indexOffset;
512 return newOffset < numItems() ? newOffset : -1;
513}
514
515void RenderListBox::panScroll(const IntPoint& panStartMousePosition)
516{
517 const int maxSpeed = 20;
518 const int iconRadius = 7;
519 const int speedReducer = 4;
520
521 // FIXME: This doesn't work correctly with transforms.
522 FloatPoint absOffset = localToAbsolute();
523
524 IntPoint lastKnownMousePosition = frame().eventHandler().lastKnownMousePosition();
525 // We need to check if the last known mouse position is out of the window. When the mouse is out of the window, the position is incoherent
526 static IntPoint previousMousePosition;
527 if (lastKnownMousePosition.y() < 0)
528 lastKnownMousePosition = previousMousePosition;
529 else
530 previousMousePosition = lastKnownMousePosition;
531
532 int yDelta = lastKnownMousePosition.y() - panStartMousePosition.y();
533
534 // If the point is too far from the center we limit the speed
535 yDelta = std::max<int>(std::min<int>(yDelta, maxSpeed), -maxSpeed);
536
537 if (abs(yDelta) < iconRadius) // at the center we let the space for the icon
538 return;
539
540 if (yDelta > 0)
541 //offsetY = view()->viewHeight();
542 absOffset.move(0, listHeight());
543 else if (yDelta < 0)
544 yDelta--;
545
546 // Let's attenuate the speed
547 yDelta /= speedReducer;
548
549 IntPoint scrollPoint(0, 0);
550 scrollPoint.setY(absOffset.y() + yDelta);
551 int newOffset = scrollToward(scrollPoint);
552 if (newOffset < 0)
553 return;
554
555 m_inAutoscroll = true;
556 selectElement().updateListBoxSelection(!selectElement().multiple());
557 m_inAutoscroll = false;
558}
559
560int RenderListBox::scrollToward(const IntPoint& destination)
561{
562 // FIXME: This doesn't work correctly with transforms.
563 FloatPoint absPos = localToAbsolute();
564 IntSize positionOffset = roundedIntSize(destination - absPos);
565
566 int rows = numVisibleItems();
567 int offset = m_indexOffset;
568
569 if (positionOffset.height() < borderTop() + paddingTop() && scrollToRevealElementAtListIndex(offset - 1))
570 return offset - 1;
571
572 if (positionOffset.height() > height() - paddingBottom() - borderBottom() && scrollToRevealElementAtListIndex(offset + rows))
573 return offset + rows - 1;
574
575 return listIndexAtOffset(positionOffset);
576}
577
578void RenderListBox::autoscroll(const IntPoint&)
579{
580 IntPoint pos = frame().view()->windowToContents(frame().eventHandler().lastKnownMousePosition());
581
582 int endIndex = scrollToward(pos);
583 if (selectElement().isDisabledFormControl())
584 return;
585
586 if (endIndex >= 0) {
587 m_inAutoscroll = true;
588
589 if (!selectElement().multiple())
590 selectElement().setActiveSelectionAnchorIndex(endIndex);
591
592 selectElement().setActiveSelectionEndIndex(endIndex);
593 selectElement().updateListBoxSelection(!selectElement().multiple());
594 m_inAutoscroll = false;
595 }
596}
597
598void RenderListBox::stopAutoscroll()
599{
600 if (selectElement().isDisabledFormControl())
601 return;
602
603 selectElement().listBoxOnChange();
604}
605
606bool RenderListBox::scrollToRevealElementAtListIndex(int index)
607{
608 if (index < 0 || index >= numItems() || listIndexIsVisible(index))
609 return false;
610
611 int newOffset;
612 if (index < m_indexOffset)
613 newOffset = index;
614 else
615 newOffset = index - numVisibleItems() + 1;
616
617 scrollToOffsetWithoutAnimation(VerticalScrollbar, newOffset);
618
619 return true;
620}
621
622bool RenderListBox::listIndexIsVisible(int index)
623{
624 int firstIndex = m_indexOfFirstVisibleItemInsidePaddingTopArea.valueOr(m_indexOffset);
625 int endIndex = m_indexOfFirstVisibleItemInsidePaddingBottomArea
626 ? m_indexOfFirstVisibleItemInsidePaddingBottomArea.value() + numberOfVisibleItemsInPaddingBottom()
627 : m_indexOffset + numVisibleItems();
628
629 return index >= firstIndex && index < endIndex;
630}
631
632bool RenderListBox::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier, Element**, RenderBox*, const IntPoint&)
633{
634 return ScrollableArea::scroll(direction, granularity, multiplier);
635}
636
637bool RenderListBox::logicalScroll(ScrollLogicalDirection direction, ScrollGranularity granularity, float multiplier, Element**)
638{
639 return ScrollableArea::scroll(logicalToPhysical(direction, style().isHorizontalWritingMode(), style().isFlippedBlocksWritingMode()), granularity, multiplier);
640}
641
642void RenderListBox::valueChanged(unsigned listIndex)
643{
644 selectElement().setSelectedIndex(selectElement().listToOptionIndex(listIndex));
645 selectElement().dispatchFormControlChangeEvent();
646}
647
648int RenderListBox::scrollSize(ScrollbarOrientation orientation) const
649{
650 return ((orientation == VerticalScrollbar) && m_vBar) ? (m_vBar->totalSize() - m_vBar->visibleSize()) : 0;
651}
652
653int RenderListBox::scrollOffset(ScrollbarOrientation) const
654{
655 return m_indexOffset;
656}
657
658ScrollPosition RenderListBox::minimumScrollPosition() const
659{
660 return { 0, 0 };
661}
662
663ScrollPosition RenderListBox::maximumScrollPosition() const
664{
665 return { 0, numItems() - numVisibleItems() };
666}
667
668void RenderListBox::setScrollOffset(const ScrollOffset& offset)
669{
670 scrollTo(offset.y());
671}
672
673int RenderListBox::maximumNumberOfItemsThatFitInPaddingBottomArea() const
674{
675 return paddingBottom() / itemHeight();
676}
677
678int RenderListBox::numberOfVisibleItemsInPaddingTop() const
679{
680 if (!m_indexOfFirstVisibleItemInsidePaddingTopArea)
681 return 0;
682
683 return m_indexOffset - m_indexOfFirstVisibleItemInsidePaddingTopArea.value();
684}
685
686int RenderListBox::numberOfVisibleItemsInPaddingBottom() const
687{
688 if (!m_indexOfFirstVisibleItemInsidePaddingBottomArea)
689 return 0;
690
691 return std::min(maximumNumberOfItemsThatFitInPaddingBottomArea(), numItems() - m_indexOffset - numVisibleItems());
692}
693
694void RenderListBox::computeFirstIndexesVisibleInPaddingTopBottomAreas()
695{
696 m_indexOfFirstVisibleItemInsidePaddingTopArea = WTF::nullopt;
697 m_indexOfFirstVisibleItemInsidePaddingBottomArea = WTF::nullopt;
698
699 int maximumNumberOfItemsThatFitInPaddingTopArea = paddingTop() / itemHeight();
700 if (maximumNumberOfItemsThatFitInPaddingTopArea) {
701 if (m_indexOffset)
702 m_indexOfFirstVisibleItemInsidePaddingTopArea = std::max(0, m_indexOffset - maximumNumberOfItemsThatFitInPaddingTopArea);
703 }
704
705 if (maximumNumberOfItemsThatFitInPaddingBottomArea()) {
706 if (numItems() > (m_indexOffset + numVisibleItems()))
707 m_indexOfFirstVisibleItemInsidePaddingBottomArea = m_indexOffset + numVisibleItems();
708 }
709}
710
711void RenderListBox::scrollTo(int newOffset)
712{
713 if (newOffset == m_indexOffset)
714 return;
715
716 m_indexOffset = newOffset;
717
718 computeFirstIndexesVisibleInPaddingTopBottomAreas();
719
720 repaint();
721 document().eventQueue().enqueueOrDispatchScrollEvent(selectElement());
722}
723
724LayoutUnit RenderListBox::itemHeight() const
725{
726 return style().fontMetrics().height() + rowSpacing;
727}
728
729int RenderListBox::verticalScrollbarWidth() const
730{
731 return m_vBar ? m_vBar->occupiedWidth() : 0;
732}
733
734// FIXME: We ignore padding in the vertical direction as far as these values are concerned, since that's
735// how the control currently paints.
736int RenderListBox::scrollWidth() const
737{
738 // There is no horizontal scrolling allowed.
739 return roundToInt(clientWidth());
740}
741
742int RenderListBox::scrollHeight() const
743{
744 return roundToInt(std::max(clientHeight(), listHeight()));
745}
746
747int RenderListBox::scrollLeft() const
748{
749 return 0;
750}
751
752void RenderListBox::setScrollLeft(int, ScrollType, ScrollClamping)
753{
754}
755
756int RenderListBox::scrollTop() const
757{
758 return m_indexOffset * itemHeight();
759}
760
761static void setupWheelEventTestTrigger(RenderListBox& renderer)
762{
763 if (!renderer.page().expectsWheelEventTriggers())
764 return;
765
766 renderer.scrollAnimator().setWheelEventTestTrigger(renderer.page().testTrigger());
767}
768
769void RenderListBox::setScrollTop(int newTop, ScrollType, ScrollClamping)
770{
771 // Determine an index and scroll to it.
772 int index = newTop / itemHeight();
773 if (index < 0 || index >= numItems() || index == m_indexOffset)
774 return;
775 setupWheelEventTestTrigger(*this);
776 scrollToOffsetWithoutAnimation(VerticalScrollbar, index);
777}
778
779bool RenderListBox::nodeAtPoint(const HitTestRequest& request, HitTestResult& result, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset, HitTestAction hitTestAction)
780{
781 if (!RenderBlockFlow::nodeAtPoint(request, result, locationInContainer, accumulatedOffset, hitTestAction))
782 return false;
783 const Vector<HTMLElement*>& listItems = selectElement().listItems();
784 int size = numItems();
785 LayoutPoint adjustedLocation = accumulatedOffset + location();
786
787 for (int i = 0; i < size; ++i) {
788 if (!itemBoundingBoxRect(adjustedLocation, i).contains(locationInContainer.point()))
789 continue;
790 if (Element* node = listItems[i]) {
791 result.setInnerNode(node);
792 if (!result.innerNonSharedNode())
793 result.setInnerNonSharedNode(node);
794 result.setLocalPoint(locationInContainer.point() - toLayoutSize(adjustedLocation));
795 break;
796 }
797 }
798
799 return true;
800}
801
802LayoutRect RenderListBox::controlClipRect(const LayoutPoint& additionalOffset) const
803{
804 // Clip against the padding box, to give <option>s and overlay scrollbar some extra space
805 // to get painted.
806 LayoutRect clipRect = paddingBoxRect();
807 if (shouldPlaceBlockDirectionScrollbarOnLeft())
808 clipRect.move(m_vBar->occupiedWidth(), 0);
809 clipRect.moveBy(additionalOffset);
810 return clipRect;
811}
812
813bool RenderListBox::isActive() const
814{
815 return page().focusController().isActive();
816}
817
818void RenderListBox::invalidateScrollbarRect(Scrollbar& scrollbar, const IntRect& rect)
819{
820 IntRect scrollRect = rect;
821 scrollRect.move(shouldPlaceBlockDirectionScrollbarOnLeft() ? borderLeft() : width() - borderRight() - scrollbar.width(), borderTop());
822 repaintRectangle(scrollRect);
823}
824
825IntRect RenderListBox::convertFromScrollbarToContainingView(const Scrollbar& scrollbar, const IntRect& scrollbarRect) const
826{
827 IntRect rect = scrollbarRect;
828 int scrollbarLeft = shouldPlaceBlockDirectionScrollbarOnLeft() ? borderLeft() : width() - borderRight() - scrollbar.width();
829 int scrollbarTop = borderTop();
830 rect.move(scrollbarLeft, scrollbarTop);
831 return view().frameView().convertFromRendererToContainingView(this, rect);
832}
833
834IntRect RenderListBox::convertFromContainingViewToScrollbar(const Scrollbar& scrollbar, const IntRect& parentRect) const
835{
836 IntRect rect = view().frameView().convertFromContainingViewToRenderer(this, parentRect);
837 int scrollbarLeft = shouldPlaceBlockDirectionScrollbarOnLeft() ? borderLeft() : width() - borderRight() - scrollbar.width();
838 int scrollbarTop = borderTop();
839 rect.move(-scrollbarLeft, -scrollbarTop);
840 return rect;
841}
842
843IntPoint RenderListBox::convertFromScrollbarToContainingView(const Scrollbar& scrollbar, const IntPoint& scrollbarPoint) const
844{
845 IntPoint point = scrollbarPoint;
846 int scrollbarLeft = shouldPlaceBlockDirectionScrollbarOnLeft() ? borderLeft() : width() - borderRight() - scrollbar.width();
847 int scrollbarTop = borderTop();
848 point.move(scrollbarLeft, scrollbarTop);
849 return view().frameView().convertFromRendererToContainingView(this, point);
850}
851
852IntPoint RenderListBox::convertFromContainingViewToScrollbar(const Scrollbar& scrollbar, const IntPoint& parentPoint) const
853{
854 IntPoint point = view().frameView().convertFromContainingViewToRenderer(this, parentPoint);
855 int scrollbarLeft = shouldPlaceBlockDirectionScrollbarOnLeft() ? borderLeft() : width() - borderRight() - scrollbar.width();
856 int scrollbarTop = borderTop();
857 point.move(-scrollbarLeft, -scrollbarTop);
858 return point;
859}
860
861IntSize RenderListBox::contentsSize() const
862{
863 return IntSize(scrollWidth(), scrollHeight());
864}
865
866IntPoint RenderListBox::lastKnownMousePosition() const
867{
868 return view().frameView().lastKnownMousePosition();
869}
870
871bool RenderListBox::isHandlingWheelEvent() const
872{
873 return view().frameView().isHandlingWheelEvent();
874}
875
876bool RenderListBox::shouldSuspendScrollAnimations() const
877{
878 return view().frameView().shouldSuspendScrollAnimations();
879}
880
881bool RenderListBox::forceUpdateScrollbarsOnMainThreadForPerformanceTesting() const
882{
883 return settings().forceUpdateScrollbarsOnMainThreadForPerformanceTesting();
884}
885
886ScrollableArea* RenderListBox::enclosingScrollableArea() const
887{
888 // FIXME: Return a RenderLayer that's scrollable.
889 return nullptr;
890}
891
892bool RenderListBox::isScrollableOrRubberbandable()
893{
894 return m_vBar;
895}
896
897bool RenderListBox::hasScrollableOrRubberbandableAncestor()
898{
899 return enclosingLayer() && enclosingLayer()->hasScrollableOrRubberbandableAncestor();
900}
901
902IntRect RenderListBox::scrollableAreaBoundingBox(bool*) const
903{
904 return absoluteBoundingBoxRect();
905}
906
907bool RenderListBox::usesMockScrollAnimator() const
908{
909 return DeprecatedGlobalSettings::usesMockScrollAnimator();
910}
911
912void RenderListBox::logMockScrollAnimatorMessage(const String& message) const
913{
914 document().addConsoleMessage(MessageSource::Other, MessageLevel::Debug, "RenderListBox: " + message);
915}
916
917Ref<Scrollbar> RenderListBox::createScrollbar()
918{
919 RefPtr<Scrollbar> widget;
920 bool hasCustomScrollbarStyle = style().hasPseudoStyle(PseudoId::Scrollbar);
921 if (hasCustomScrollbarStyle)
922 widget = RenderScrollbar::createCustomScrollbar(*this, VerticalScrollbar, &selectElement());
923 else {
924 widget = Scrollbar::createNativeScrollbar(*this, VerticalScrollbar, theme().scrollbarControlSizeForPart(ListboxPart));
925 didAddScrollbar(widget.get(), VerticalScrollbar);
926 if (page().expectsWheelEventTriggers())
927 scrollAnimator().setWheelEventTestTrigger(page().testTrigger());
928 }
929 view().frameView().addChild(*widget);
930 return widget.releaseNonNull();
931}
932
933void RenderListBox::destroyScrollbar()
934{
935 if (!m_vBar)
936 return;
937
938 if (!m_vBar->isCustomScrollbar())
939 ScrollableArea::willRemoveScrollbar(m_vBar.get(), VerticalScrollbar);
940 m_vBar->removeFromParent();
941 m_vBar = nullptr;
942}
943
944void RenderListBox::setHasVerticalScrollbar(bool hasScrollbar)
945{
946 if (hasScrollbar == (m_vBar != nullptr))
947 return;
948
949 if (hasScrollbar)
950 m_vBar = createScrollbar();
951 else
952 destroyScrollbar();
953
954 if (m_vBar)
955 m_vBar->styleChanged();
956
957 document().invalidateScrollbarDependentRegions();
958}
959
960bool RenderListBox::scrolledToTop() const
961{
962 if (Scrollbar* vbar = verticalScrollbar())
963 return vbar->value() <= 0;
964
965 return true;
966}
967
968bool RenderListBox::scrolledToBottom() const
969{
970 Scrollbar* vbar = verticalScrollbar();
971 if (!vbar)
972 return true;
973
974 return vbar->value() >= vbar->maximum();
975}
976
977bool RenderListBox::scrolledToLeft() const
978{
979 // We do not scroll horizontally in a select element, so always report
980 // that we are at the full extent of the scroll.
981 return true;
982}
983
984bool RenderListBox::scrolledToRight() const
985{
986 // We do not scroll horizontally in a select element, so always report
987 // that we are at the full extent of the scroll.
988 return true;
989}
990
991} // namespace WebCore
992