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 | |
70 | namespace WebCore { |
71 | |
72 | using namespace HTMLNames; |
73 | |
74 | WTF_MAKE_ISO_ALLOCATED_IMPL(RenderListBox); |
75 | |
76 | const int rowSpacing = 1; |
77 | |
78 | const int optionsSpacingHorizontal = 2; |
79 | |
80 | // The minSize constant was originally defined to render scrollbars correctly. |
81 | // This might vary for different platforms. |
82 | const int minSize = 4; |
83 | |
84 | // Default size when the multiple attribute is present but size attribute is absent. |
85 | const 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. |
89 | const int baselineAdjustment = 7; |
90 | |
91 | RenderListBox::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 | |
102 | RenderListBox::~RenderListBox() |
103 | { |
104 | // Do not add any code here. Add it to willBeDestroyed() instead. |
105 | } |
106 | |
107 | void RenderListBox::willBeDestroyed() |
108 | { |
109 | setHasVerticalScrollbar(false); |
110 | view().frameView().removeScrollableArea(this); |
111 | RenderBlockFlow::willBeDestroyed(); |
112 | } |
113 | |
114 | HTMLSelectElement& RenderListBox::selectElement() const |
115 | { |
116 | return downcast<HTMLSelectElement>(nodeForNonAnonymous()); |
117 | } |
118 | |
119 | static 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 | |
128 | void 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 | |
167 | void 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 | |
181 | void 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 | |
203 | void 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 | |
212 | void 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 | |
221 | void 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 | |
251 | int 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 | |
260 | int 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 | |
270 | int RenderListBox::numItems() const |
271 | { |
272 | return selectElement().listItems().size(); |
273 | } |
274 | |
275 | LayoutUnit RenderListBox::listHeight() const |
276 | { |
277 | return itemHeight() * numItems() - rowSpacing; |
278 | } |
279 | |
280 | RenderBox::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 | |
288 | int RenderListBox::baselinePosition(FontBaseline baselineType, bool firstLine, LineDirectionMode lineDirection, LinePositionMode linePositionMode) const |
289 | { |
290 | return RenderBox::baselinePosition(baselineType, firstLine, lineDirection, linePositionMode) - baselineAdjustment; |
291 | } |
292 | |
293 | LayoutRect 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 | |
302 | void 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 | |
311 | void 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 | |
348 | void 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 | |
373 | void 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 | |
387 | static 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 | |
407 | void 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 | |
455 | void 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 | |
479 | bool 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 | |
497 | int 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 | |
515 | void 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 | |
560 | int 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 | |
578 | void 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 | |
598 | void RenderListBox::stopAutoscroll() |
599 | { |
600 | if (selectElement().isDisabledFormControl()) |
601 | return; |
602 | |
603 | selectElement().listBoxOnChange(); |
604 | } |
605 | |
606 | bool 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 | |
622 | bool 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 | |
632 | bool RenderListBox::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier, Element**, RenderBox*, const IntPoint&) |
633 | { |
634 | return ScrollableArea::scroll(direction, granularity, multiplier); |
635 | } |
636 | |
637 | bool RenderListBox::logicalScroll(ScrollLogicalDirection direction, ScrollGranularity granularity, float multiplier, Element**) |
638 | { |
639 | return ScrollableArea::scroll(logicalToPhysical(direction, style().isHorizontalWritingMode(), style().isFlippedBlocksWritingMode()), granularity, multiplier); |
640 | } |
641 | |
642 | void RenderListBox::valueChanged(unsigned listIndex) |
643 | { |
644 | selectElement().setSelectedIndex(selectElement().listToOptionIndex(listIndex)); |
645 | selectElement().dispatchFormControlChangeEvent(); |
646 | } |
647 | |
648 | int RenderListBox::scrollSize(ScrollbarOrientation orientation) const |
649 | { |
650 | return ((orientation == VerticalScrollbar) && m_vBar) ? (m_vBar->totalSize() - m_vBar->visibleSize()) : 0; |
651 | } |
652 | |
653 | int RenderListBox::scrollOffset(ScrollbarOrientation) const |
654 | { |
655 | return m_indexOffset; |
656 | } |
657 | |
658 | ScrollPosition RenderListBox::minimumScrollPosition() const |
659 | { |
660 | return { 0, 0 }; |
661 | } |
662 | |
663 | ScrollPosition RenderListBox::maximumScrollPosition() const |
664 | { |
665 | return { 0, numItems() - numVisibleItems() }; |
666 | } |
667 | |
668 | void RenderListBox::setScrollOffset(const ScrollOffset& offset) |
669 | { |
670 | scrollTo(offset.y()); |
671 | } |
672 | |
673 | int RenderListBox::maximumNumberOfItemsThatFitInPaddingBottomArea() const |
674 | { |
675 | return paddingBottom() / itemHeight(); |
676 | } |
677 | |
678 | int RenderListBox::numberOfVisibleItemsInPaddingTop() const |
679 | { |
680 | if (!m_indexOfFirstVisibleItemInsidePaddingTopArea) |
681 | return 0; |
682 | |
683 | return m_indexOffset - m_indexOfFirstVisibleItemInsidePaddingTopArea.value(); |
684 | } |
685 | |
686 | int RenderListBox::numberOfVisibleItemsInPaddingBottom() const |
687 | { |
688 | if (!m_indexOfFirstVisibleItemInsidePaddingBottomArea) |
689 | return 0; |
690 | |
691 | return std::min(maximumNumberOfItemsThatFitInPaddingBottomArea(), numItems() - m_indexOffset - numVisibleItems()); |
692 | } |
693 | |
694 | void 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 | |
711 | void 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 | |
724 | LayoutUnit RenderListBox::itemHeight() const |
725 | { |
726 | return style().fontMetrics().height() + rowSpacing; |
727 | } |
728 | |
729 | int 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. |
736 | int RenderListBox::scrollWidth() const |
737 | { |
738 | // There is no horizontal scrolling allowed. |
739 | return roundToInt(clientWidth()); |
740 | } |
741 | |
742 | int RenderListBox::scrollHeight() const |
743 | { |
744 | return roundToInt(std::max(clientHeight(), listHeight())); |
745 | } |
746 | |
747 | int RenderListBox::scrollLeft() const |
748 | { |
749 | return 0; |
750 | } |
751 | |
752 | void RenderListBox::setScrollLeft(int, ScrollType, ScrollClamping) |
753 | { |
754 | } |
755 | |
756 | int RenderListBox::scrollTop() const |
757 | { |
758 | return m_indexOffset * itemHeight(); |
759 | } |
760 | |
761 | static void setupWheelEventTestTrigger(RenderListBox& renderer) |
762 | { |
763 | if (!renderer.page().expectsWheelEventTriggers()) |
764 | return; |
765 | |
766 | renderer.scrollAnimator().setWheelEventTestTrigger(renderer.page().testTrigger()); |
767 | } |
768 | |
769 | void 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 | |
779 | bool 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 | |
802 | LayoutRect 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 | |
813 | bool RenderListBox::isActive() const |
814 | { |
815 | return page().focusController().isActive(); |
816 | } |
817 | |
818 | void 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 | |
825 | IntRect 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 | |
834 | IntRect 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 | |
843 | IntPoint 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 | |
852 | IntPoint 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 | |
861 | IntSize RenderListBox::contentsSize() const |
862 | { |
863 | return IntSize(scrollWidth(), scrollHeight()); |
864 | } |
865 | |
866 | IntPoint RenderListBox::lastKnownMousePosition() const |
867 | { |
868 | return view().frameView().lastKnownMousePosition(); |
869 | } |
870 | |
871 | bool RenderListBox::isHandlingWheelEvent() const |
872 | { |
873 | return view().frameView().isHandlingWheelEvent(); |
874 | } |
875 | |
876 | bool RenderListBox::shouldSuspendScrollAnimations() const |
877 | { |
878 | return view().frameView().shouldSuspendScrollAnimations(); |
879 | } |
880 | |
881 | bool RenderListBox::forceUpdateScrollbarsOnMainThreadForPerformanceTesting() const |
882 | { |
883 | return settings().forceUpdateScrollbarsOnMainThreadForPerformanceTesting(); |
884 | } |
885 | |
886 | ScrollableArea* RenderListBox::enclosingScrollableArea() const |
887 | { |
888 | // FIXME: Return a RenderLayer that's scrollable. |
889 | return nullptr; |
890 | } |
891 | |
892 | bool RenderListBox::isScrollableOrRubberbandable() |
893 | { |
894 | return m_vBar; |
895 | } |
896 | |
897 | bool RenderListBox::hasScrollableOrRubberbandableAncestor() |
898 | { |
899 | return enclosingLayer() && enclosingLayer()->hasScrollableOrRubberbandableAncestor(); |
900 | } |
901 | |
902 | IntRect RenderListBox::scrollableAreaBoundingBox(bool*) const |
903 | { |
904 | return absoluteBoundingBoxRect(); |
905 | } |
906 | |
907 | bool RenderListBox::usesMockScrollAnimator() const |
908 | { |
909 | return DeprecatedGlobalSettings::usesMockScrollAnimator(); |
910 | } |
911 | |
912 | void RenderListBox::logMockScrollAnimatorMessage(const String& message) const |
913 | { |
914 | document().addConsoleMessage(MessageSource::Other, MessageLevel::Debug, "RenderListBox: " + message); |
915 | } |
916 | |
917 | Ref<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 | |
933 | void 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 | |
944 | void 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 | |
960 | bool RenderListBox::scrolledToTop() const |
961 | { |
962 | if (Scrollbar* vbar = verticalScrollbar()) |
963 | return vbar->value() <= 0; |
964 | |
965 | return true; |
966 | } |
967 | |
968 | bool RenderListBox::scrolledToBottom() const |
969 | { |
970 | Scrollbar* vbar = verticalScrollbar(); |
971 | if (!vbar) |
972 | return true; |
973 | |
974 | return vbar->value() >= vbar->maximum(); |
975 | } |
976 | |
977 | bool 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 | |
984 | bool 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 | |