| OLD | NEW |
| (Empty) |
| 1 /* | |
| 2 * Copyright (c) 2011, Google Inc. All rights reserved. | |
| 3 * | |
| 4 * Redistribution and use in source and binary forms, with or without | |
| 5 * modification, are permitted provided that the following conditions are | |
| 6 * met: | |
| 7 * | |
| 8 * * Redistributions of source code must retain the above copyright | |
| 9 * notice, this list of conditions and the following disclaimer. | |
| 10 * * Redistributions in binary form must reproduce the above | |
| 11 * copyright notice, this list of conditions and the following disclaimer | |
| 12 * in the documentation and/or other materials provided with the | |
| 13 * distribution. | |
| 14 * * Neither the name of Google Inc. nor the names of its | |
| 15 * contributors may be used to endorse or promote products derived from | |
| 16 * this software without specific prior written permission. | |
| 17 * | |
| 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 29 */ | |
| 30 | |
| 31 #include "config.h" | |
| 32 #include "web/PopupListBox.h" | |
| 33 | |
| 34 #include "core/CSSValueKeywords.h" | |
| 35 #include "core/html/forms/PopupMenuClient.h" | |
| 36 #include "core/layout/LayoutTheme.h" | |
| 37 #include "core/paint/ScrollRecorder.h" | |
| 38 #include "core/paint/TransformRecorder.h" | |
| 39 #include "platform/KeyboardCodes.h" | |
| 40 #include "platform/PlatformGestureEvent.h" | |
| 41 #include "platform/PlatformKeyboardEvent.h" | |
| 42 #include "platform/PlatformMouseEvent.h" | |
| 43 #include "platform/PlatformTouchEvent.h" | |
| 44 #include "platform/PlatformWheelEvent.h" | |
| 45 #include "platform/RuntimeEnabledFeatures.h" | |
| 46 #include "platform/fonts/Font.h" | |
| 47 #include "platform/fonts/FontCache.h" | |
| 48 #include "platform/fonts/FontSelector.h" | |
| 49 #include "platform/geometry/IntRect.h" | |
| 50 #include "platform/graphics/GraphicsContext.h" | |
| 51 #include "platform/graphics/GraphicsContextStateSaver.h" | |
| 52 #include "platform/graphics/paint/ClipRecorder.h" | |
| 53 #include "platform/graphics/paint/DrawingRecorder.h" | |
| 54 #include "platform/scroll/ScrollbarTheme.h" | |
| 55 #include "platform/text/StringTruncator.h" | |
| 56 #include "platform/text/TextRun.h" | |
| 57 #include "web/PopupContainer.h" | |
| 58 #include "web/PopupContainerClient.h" | |
| 59 #include "web/PopupMenuChromium.h" | |
| 60 #include "wtf/ASCIICType.h" | |
| 61 #include "wtf/CurrentTime.h" | |
| 62 #include <limits> | |
| 63 | |
| 64 namespace blink { | |
| 65 | |
| 66 using namespace WTF::Unicode; | |
| 67 | |
| 68 const int PopupListBox::defaultMaxHeight = 500; | |
| 69 static const int maxVisibleRows = 20; | |
| 70 static const int minEndOfLinePadding = 2; | |
| 71 static const TimeStamp typeAheadTimeoutMs = 1000; | |
| 72 | |
| 73 PopupListBox::PopupListBox(PopupMenuClient* client, bool deviceSupportsTouch, Po
pupContainer* container) | |
| 74 : m_deviceSupportsTouch(deviceSupportsTouch) | |
| 75 , m_selectedIndex(0) | |
| 76 , m_visibleRows(0) | |
| 77 , m_baseWidth(0) | |
| 78 , m_maxHeight(defaultMaxHeight) | |
| 79 , m_popupClient(client) | |
| 80 , m_repeatingChar(0) | |
| 81 , m_lastCharTime(0) | |
| 82 , m_maxWindowWidth(std::numeric_limits<int>::max()) | |
| 83 , m_container(container) | |
| 84 { | |
| 85 } | |
| 86 | |
| 87 PopupListBox::~PopupListBox() | |
| 88 { | |
| 89 // Oilpan: the scrollbars of the ScrollView are self-sufficient, | |
| 90 // capable of detaching themselves from their animator on | |
| 91 // finalization. | |
| 92 #if !ENABLE(OILPAN) | |
| 93 setHasVerticalScrollbar(false); | |
| 94 #endif | |
| 95 } | |
| 96 | |
| 97 DEFINE_TRACE(PopupListBox) | |
| 98 { | |
| 99 visitor->trace(m_capturingScrollbar); | |
| 100 visitor->trace(m_lastScrollbarUnderMouse); | |
| 101 visitor->trace(m_focusedElement); | |
| 102 visitor->trace(m_container); | |
| 103 visitor->trace(m_verticalScrollbar); | |
| 104 Widget::trace(visitor); | |
| 105 } | |
| 106 | |
| 107 bool PopupListBox::handleMouseDownEvent(const PlatformMouseEvent& event) | |
| 108 { | |
| 109 Scrollbar* scrollbar = scrollbarAtRootFramePoint(event.position()); | |
| 110 if (scrollbar) { | |
| 111 m_capturingScrollbar = scrollbar; | |
| 112 m_capturingScrollbar->mouseDown(event); | |
| 113 return true; | |
| 114 } | |
| 115 | |
| 116 if (!isPointInBounds(event.position())) | |
| 117 cancel(); | |
| 118 | |
| 119 return true; | |
| 120 } | |
| 121 | |
| 122 bool PopupListBox::handleMouseMoveEvent(const PlatformMouseEvent& event) | |
| 123 { | |
| 124 if (m_capturingScrollbar) { | |
| 125 m_capturingScrollbar->mouseMoved(event); | |
| 126 return true; | |
| 127 } | |
| 128 | |
| 129 Scrollbar* scrollbar = scrollbarAtRootFramePoint(event.position()); | |
| 130 if (m_lastScrollbarUnderMouse != scrollbar) { | |
| 131 // Send mouse exited to the old scrollbar. | |
| 132 if (m_lastScrollbarUnderMouse) | |
| 133 m_lastScrollbarUnderMouse->mouseExited(); | |
| 134 m_lastScrollbarUnderMouse = scrollbar; | |
| 135 } | |
| 136 | |
| 137 if (scrollbar) { | |
| 138 scrollbar->mouseMoved(event); | |
| 139 return true; | |
| 140 } | |
| 141 | |
| 142 if (!isPointInBounds(event.position())) | |
| 143 return false; | |
| 144 | |
| 145 selectIndex(pointToRowIndex(event.position())); | |
| 146 return true; | |
| 147 } | |
| 148 | |
| 149 bool PopupListBox::handleMouseReleaseEvent(const PlatformMouseEvent& event) | |
| 150 { | |
| 151 if (m_capturingScrollbar) { | |
| 152 m_capturingScrollbar->mouseUp(event); | |
| 153 m_capturingScrollbar = nullptr; | |
| 154 return true; | |
| 155 } | |
| 156 | |
| 157 if (!isPointInBounds(event.position())) | |
| 158 return true; | |
| 159 | |
| 160 if (acceptIndex(pointToRowIndex(event.position())) && m_focusedElement) { | |
| 161 m_focusedElement->dispatchMouseEvent(event, EventTypeNames::mouseup); | |
| 162 m_focusedElement->dispatchMouseEvent(event, EventTypeNames::click); | |
| 163 | |
| 164 // Clear m_focusedElement here, because we cannot clear in hidePopup() | |
| 165 // which is called before dispatchMouseEvent() is called. | |
| 166 m_focusedElement = nullptr; | |
| 167 } | |
| 168 | |
| 169 return true; | |
| 170 } | |
| 171 | |
| 172 bool PopupListBox::handleWheelEvent(const PlatformWheelEvent& event) | |
| 173 { | |
| 174 if (!isPointInBounds(event.position())) { | |
| 175 cancel(); | |
| 176 return true; | |
| 177 } | |
| 178 | |
| 179 ScrollableArea::handleWheel(event); | |
| 180 return true; | |
| 181 } | |
| 182 | |
| 183 bool PopupListBox::handleTouchEvent(const PlatformTouchEvent&) | |
| 184 { | |
| 185 return false; | |
| 186 } | |
| 187 | |
| 188 bool PopupListBox::handleGestureEvent(const PlatformGestureEvent&) | |
| 189 { | |
| 190 return false; | |
| 191 } | |
| 192 | |
| 193 static bool isCharacterTypeEvent(const PlatformKeyboardEvent& event) | |
| 194 { | |
| 195 // Check whether the event is a character-typed event or not. | |
| 196 // We use RawKeyDown/Char/KeyUp event scheme on all platforms, | |
| 197 // so PlatformKeyboardEvent::Char (not RawKeyDown) type event | |
| 198 // is considered as character type event. | |
| 199 return event.type() == PlatformEvent::Char; | |
| 200 } | |
| 201 | |
| 202 bool PopupListBox::handleKeyEvent(const PlatformKeyboardEvent& event) | |
| 203 { | |
| 204 if (event.type() == PlatformEvent::KeyUp) | |
| 205 return true; | |
| 206 | |
| 207 if (!numItems() && event.windowsVirtualKeyCode() != VKEY_ESCAPE) | |
| 208 return true; | |
| 209 | |
| 210 switch (event.windowsVirtualKeyCode()) { | |
| 211 case VKEY_ESCAPE: | |
| 212 cancel(); // may delete this | |
| 213 return true; | |
| 214 case VKEY_RETURN: | |
| 215 if (m_selectedIndex == -1) { | |
| 216 hidePopup(); | |
| 217 // Don't eat the enter if nothing is selected. | |
| 218 return false; | |
| 219 } | |
| 220 acceptIndex(m_selectedIndex); // may delete this | |
| 221 return true; | |
| 222 case VKEY_UP: | |
| 223 selectPreviousRow(); | |
| 224 break; | |
| 225 case VKEY_DOWN: | |
| 226 selectNextRow(); | |
| 227 break; | |
| 228 case VKEY_PRIOR: | |
| 229 adjustSelectedIndex(-m_visibleRows); | |
| 230 break; | |
| 231 case VKEY_NEXT: | |
| 232 adjustSelectedIndex(m_visibleRows); | |
| 233 break; | |
| 234 case VKEY_HOME: | |
| 235 adjustSelectedIndex(-m_selectedIndex); | |
| 236 break; | |
| 237 case VKEY_END: | |
| 238 adjustSelectedIndex(m_items.size()); | |
| 239 break; | |
| 240 default: | |
| 241 if (!event.ctrlKey() && !event.altKey() && !event.metaKey() | |
| 242 && isPrintableChar(event.windowsVirtualKeyCode()) | |
| 243 && isCharacterTypeEvent(event)) | |
| 244 typeAheadFind(event); | |
| 245 break; | |
| 246 } | |
| 247 | |
| 248 if (event.altKey() && (event.keyIdentifier() == "Down" || event.keyIdentifie
r() == "Up")) { | |
| 249 hidePopup(); | |
| 250 return true; | |
| 251 } | |
| 252 | |
| 253 m_popupClient->provisionalSelectionChanged(m_selectedIndex); | |
| 254 if (event.windowsVirtualKeyCode() == VKEY_TAB) { | |
| 255 // TAB is a special case as it should select the current item if any and | |
| 256 // advance focus. | |
| 257 if (m_selectedIndex >= 0) { | |
| 258 acceptIndex(m_selectedIndex); // May delete us. | |
| 259 // Return false so the TAB key event is propagated to the page. | |
| 260 return false; | |
| 261 } | |
| 262 cancel(); | |
| 263 // Return false so the TAB key event is propagated to the page. | |
| 264 return false; | |
| 265 } | |
| 266 | |
| 267 return true; | |
| 268 } | |
| 269 | |
| 270 HostWindow* PopupListBox::hostWindow() const | |
| 271 { | |
| 272 // Our parent is the root FrameView, so it is the one that has a | |
| 273 // HostWindow. FrameView::hostWindow() works similarly. | |
| 274 return parent() ? parent()->hostWindow() : 0; | |
| 275 } | |
| 276 | |
| 277 bool PopupListBox::shouldPlaceVerticalScrollbarOnLeft() const | |
| 278 { | |
| 279 return m_popupClient->menuStyle().textDirection() == RTL; | |
| 280 } | |
| 281 | |
| 282 // From HTMLSelectElement.cpp | |
| 283 static String stripLeadingWhiteSpace(const String& string) | |
| 284 { | |
| 285 int length = string.length(); | |
| 286 int i; | |
| 287 for (i = 0; i < length; ++i) | |
| 288 if (string[i] != noBreakSpaceCharacter | |
| 289 && !isSpaceOrNewline(string[i])) | |
| 290 break; | |
| 291 | |
| 292 return string.substring(i, length - i); | |
| 293 } | |
| 294 | |
| 295 // From HTMLSelectElement.cpp, with modifications | |
| 296 void PopupListBox::typeAheadFind(const PlatformKeyboardEvent& event) | |
| 297 { | |
| 298 TimeStamp now = static_cast<TimeStamp>(currentTime() * 1000.0f); | |
| 299 TimeStamp delta = now - m_lastCharTime; | |
| 300 | |
| 301 // Reset the time when user types in a character. The time gap between | |
| 302 // last character and the current character is used to indicate whether | |
| 303 // user typed in a string or just a character as the search prefix. | |
| 304 m_lastCharTime = now; | |
| 305 | |
| 306 UChar c = event.windowsVirtualKeyCode(); | |
| 307 | |
| 308 String prefix; | |
| 309 int searchStartOffset = 1; | |
| 310 if (delta > typeAheadTimeoutMs) { | |
| 311 m_typedString = prefix = String(&c, 1); | |
| 312 m_repeatingChar = c; | |
| 313 } else { | |
| 314 m_typedString.append(c); | |
| 315 | |
| 316 if (c == m_repeatingChar) { | |
| 317 // The user is likely trying to cycle through all the items starting | |
| 318 // with this character, so just search on the character. | |
| 319 prefix = String(&c, 1); | |
| 320 } else { | |
| 321 m_repeatingChar = 0; | |
| 322 prefix = m_typedString; | |
| 323 searchStartOffset = 0; | |
| 324 } | |
| 325 } | |
| 326 | |
| 327 // Compute a case-folded copy of the prefix string before beginning the | |
| 328 // search for a matching element. This code uses foldCase to work around the | |
| 329 // fact that String::startWith does not fold non-ASCII characters. This code | |
| 330 // can be changed to use startWith once that is fixed. | |
| 331 String prefixWithCaseFolded(prefix.foldCase()); | |
| 332 int itemCount = numItems(); | |
| 333 int index = (max(0, m_selectedIndex) + searchStartOffset) % itemCount; | |
| 334 for (int i = 0; i < itemCount; i++, index = (index + 1) % itemCount) { | |
| 335 if (!isSelectableItem(index)) | |
| 336 continue; | |
| 337 | |
| 338 if (stripLeadingWhiteSpace(m_items[index]->label).foldCase().startsWith(
prefixWithCaseFolded)) { | |
| 339 selectIndex(index); | |
| 340 return; | |
| 341 } | |
| 342 } | |
| 343 } | |
| 344 | |
| 345 void PopupListBox::paint(GraphicsContext* gc, const IntRect& rect) | |
| 346 { | |
| 347 ClipRecorder frameClip(*gc, *this, DisplayItem::ClipPopupListBoxFrame, Layou
tRect(frameRect())); | |
| 348 TransformRecorder transformRecorder(*gc, *this, AffineTransform::translation
(x(), y())); | |
| 349 IntRect paintRect = intersection(rect, frameRect()); | |
| 350 paintRect.moveBy(-location()); | |
| 351 | |
| 352 if (numItems()) { | |
| 353 IntSize scrollOffset = toIntSize(m_scrollOffset); | |
| 354 // FIXME: Calling this part of the scroll might cause issues later on | |
| 355 // depending on how the compositor winds up using this information. | |
| 356 // We may need to use an additional TransformRecorder instead, if that | |
| 357 // happens. | |
| 358 if (shouldPlaceVerticalScrollbarOnLeft() && verticalScrollbar() && !vert
icalScrollbar()->isOverlayScrollbar()) | |
| 359 scrollOffset.expand(-verticalScrollbar()->width(), 0); | |
| 360 ScrollRecorder scroll(*gc, *this, PaintPhase::PaintPhaseForeground, scro
llOffset); | |
| 361 IntRect scrolledPaintRect = paintRect; | |
| 362 scrolledPaintRect.move(scrollOffset); | |
| 363 | |
| 364 // FIXME: Can we optimize scrolling to not require repainting the entire | |
| 365 // window? Should we? | |
| 366 for (int i = 0; i < numItems(); ++i) | |
| 367 paintRow(gc, scrolledPaintRect, i); | |
| 368 } else { | |
| 369 // Special case for an empty popup. | |
| 370 DrawingRecorder drawingRecorder(*gc, *this, DisplayItem::PopupListBoxBac
kground, boundsRect()); | |
| 371 if (!drawingRecorder.canUseCachedDrawing()) | |
| 372 gc->fillRect(boundsRect(), Color::white); | |
| 373 } | |
| 374 | |
| 375 if (verticalScrollbar()) | |
| 376 verticalScrollbar()->paint(gc, paintRect); | |
| 377 } | |
| 378 | |
| 379 static const int separatorPadding = 4; | |
| 380 static const int separatorHeight = 1; | |
| 381 static const int minRowHeight = 0; | |
| 382 static const int optionRowHeightForTouch = 28; | |
| 383 | |
| 384 void PopupListBox::paintRow(GraphicsContext* gc, const IntRect& rect, int rowInd
ex) | |
| 385 { | |
| 386 // This code is based largely on LayoutListBox::paint* methods. | |
| 387 | |
| 388 IntRect rowRect = getRowBounds(rowIndex); | |
| 389 if (!rowRect.intersects(rect)) | |
| 390 return; | |
| 391 | |
| 392 DrawingRecorder drawingRecorder(*gc, *m_items[rowIndex], DisplayItem::PopupL
istBoxRow, rowRect); | |
| 393 if (drawingRecorder.canUseCachedDrawing()) | |
| 394 return; | |
| 395 | |
| 396 PopupMenuStyle style = m_popupClient->itemStyle(rowIndex); | |
| 397 | |
| 398 // Paint background | |
| 399 Color backColor, textColor, labelColor; | |
| 400 if (rowIndex == m_selectedIndex) { | |
| 401 backColor = LayoutTheme::theme().activeListBoxSelectionBackgroundColor()
; | |
| 402 textColor = LayoutTheme::theme().activeListBoxSelectionForegroundColor()
; | |
| 403 labelColor = textColor; | |
| 404 } else { | |
| 405 backColor = style.backgroundColor(); | |
| 406 textColor = style.foregroundColor(); | |
| 407 #if OS(LINUX) || OS(ANDROID) | |
| 408 // On other platforms, the <option> background color is the same as the | |
| 409 // <select> background color. On Linux, that makes the <option> | |
| 410 // background color very dark, so by default, try to use a lighter | |
| 411 // background color for <option>s. | |
| 412 if (style.backgroundColorType() == PopupMenuStyle::DefaultBackgroundColo
r && LayoutTheme::theme().systemColor(CSSValueButtonface) == backColor) | |
| 413 backColor = LayoutTheme::theme().systemColor(CSSValueMenu); | |
| 414 #endif | |
| 415 | |
| 416 // FIXME: for now the label color is hard-coded. It should be added to | |
| 417 // the PopupMenuStyle. | |
| 418 labelColor = Color(115, 115, 115); | |
| 419 } | |
| 420 | |
| 421 // If we have a transparent background, make sure it has a color to blend | |
| 422 // against. | |
| 423 if (backColor.hasAlpha()) | |
| 424 gc->fillRect(rowRect, Color::white); | |
| 425 | |
| 426 gc->fillRect(rowRect, backColor); | |
| 427 | |
| 428 if (m_popupClient->itemIsSeparator(rowIndex)) { | |
| 429 IntRect separatorRect( | |
| 430 rowRect.x() + separatorPadding, | |
| 431 rowRect.y() + (rowRect.height() - separatorHeight) / 2, | |
| 432 rowRect.width() - 2 * separatorPadding, separatorHeight); | |
| 433 gc->fillRect(separatorRect, textColor); | |
| 434 return; | |
| 435 } | |
| 436 | |
| 437 if (!style.isVisible()) | |
| 438 return; | |
| 439 | |
| 440 gc->setFillColor(textColor); | |
| 441 | |
| 442 FontCachePurgePreventer fontCachePurgePreventer; | |
| 443 | |
| 444 Font itemFont = getRowFont(rowIndex); | |
| 445 // FIXME: http://crbug.com/19872 We should get the padding of individual opt
ion | |
| 446 // elements. This probably implies changes to PopupMenuClient. | |
| 447 bool rightAligned = m_popupClient->menuStyle().textDirection() == RTL; | |
| 448 int textX = 0; | |
| 449 int maxWidth = 0; | |
| 450 if (rightAligned) { | |
| 451 maxWidth = rowRect.width() - max<int>(0, m_popupClient->clientPaddingRig
ht()); | |
| 452 } else { | |
| 453 textX = max<int>(0, m_popupClient->clientPaddingLeft()); | |
| 454 maxWidth = rowRect.width() - textX; | |
| 455 } | |
| 456 // Prepare text to be drawn. | |
| 457 String itemText = m_popupClient->itemText(rowIndex); | |
| 458 | |
| 459 // Prepare the directionality to draw text. | |
| 460 TextRun textRun(itemText, 0, 0, TextRun::AllowTrailingExpansion, style.textD
irection(), style.hasTextDirectionOverride()); | |
| 461 // If the text is right-to-left, make it right-aligned by adjusting its | |
| 462 // beginning position. | |
| 463 if (rightAligned) | |
| 464 textX += maxWidth - itemFont.width(textRun); | |
| 465 | |
| 466 // Draw the item text. | |
| 467 int textY = rowRect.y() + itemFont.fontMetrics().ascent() + (rowRect.height(
) - itemFont.fontMetrics().height()) / 2; | |
| 468 TextRunPaintInfo textRunPaintInfo(textRun); | |
| 469 textRunPaintInfo.bounds = rowRect; | |
| 470 gc->drawBidiText(itemFont, textRunPaintInfo, IntPoint(textX, textY)); | |
| 471 } | |
| 472 | |
| 473 Font PopupListBox::getRowFont(int rowIndex) const | |
| 474 { | |
| 475 Font itemFont = m_popupClient->itemStyle(rowIndex).font(); | |
| 476 if (m_popupClient->itemIsLabel(rowIndex)) { | |
| 477 // Bold-ify labels (ie, an <optgroup> heading). | |
| 478 FontDescription d = itemFont.fontDescription(); | |
| 479 d.setWeight(FontWeightBold); | |
| 480 Font font(d); | |
| 481 font.update(nullptr); | |
| 482 return font; | |
| 483 } | |
| 484 | |
| 485 return itemFont; | |
| 486 } | |
| 487 | |
| 488 void PopupListBox::cancel() | |
| 489 { | |
| 490 RefPtrWillBeRawPtr<PopupListBox> protect(this); | |
| 491 | |
| 492 hidePopup(); | |
| 493 | |
| 494 if (m_popupClient) | |
| 495 m_popupClient->popupDidCancel(); | |
| 496 } | |
| 497 | |
| 498 int PopupListBox::pointToRowIndex(const IntPoint& point) | |
| 499 { | |
| 500 int y = scrollY() + point.y(); | |
| 501 | |
| 502 // FIXME: binary search if perf matters. | |
| 503 for (int i = 0; i < numItems(); ++i) { | |
| 504 if (y < m_items[i]->yOffset) | |
| 505 return i-1; | |
| 506 } | |
| 507 | |
| 508 // Last item? | |
| 509 if (y < contentsSize().height()) | |
| 510 return m_items.size()-1; | |
| 511 | |
| 512 return -1; | |
| 513 } | |
| 514 | |
| 515 bool PopupListBox::acceptIndex(int index) | |
| 516 { | |
| 517 if (index >= numItems()) | |
| 518 return false; | |
| 519 | |
| 520 if (index < 0) { | |
| 521 if (m_popupClient) { | |
| 522 // Enter pressed with no selection, just close the popup. | |
| 523 hidePopup(); | |
| 524 } | |
| 525 return false; | |
| 526 } | |
| 527 | |
| 528 if (isSelectableItem(index)) { | |
| 529 RefPtrWillBeRawPtr<PopupListBox> protect(this); | |
| 530 | |
| 531 // Hide ourselves first since valueChanged may have numerous side-effect
s. | |
| 532 hidePopup(); | |
| 533 | |
| 534 // Tell the <select> PopupMenuClient what index was selected. | |
| 535 m_popupClient->valueChanged(index); | |
| 536 | |
| 537 return true; | |
| 538 } | |
| 539 | |
| 540 return false; | |
| 541 } | |
| 542 | |
| 543 void PopupListBox::selectIndex(int index) | |
| 544 { | |
| 545 if (index < 0 || index >= numItems()) | |
| 546 return; | |
| 547 | |
| 548 bool isSelectable = isSelectableItem(index); | |
| 549 if (index != m_selectedIndex && isSelectable) { | |
| 550 invalidateRow(m_selectedIndex); | |
| 551 m_selectedIndex = index; | |
| 552 invalidateRow(m_selectedIndex); | |
| 553 | |
| 554 scrollToRevealSelection(); | |
| 555 m_popupClient->selectionChanged(m_selectedIndex); | |
| 556 } else if (!isSelectable) | |
| 557 clearSelection(); | |
| 558 } | |
| 559 | |
| 560 int PopupListBox::getRowHeight(int index) const | |
| 561 { | |
| 562 int minimumHeight = m_deviceSupportsTouch ? optionRowHeightForTouch : minRow
Height; | |
| 563 | |
| 564 if (index < 0 || m_popupClient->itemStyle(index).isDisplayNone()) | |
| 565 return minimumHeight; | |
| 566 | |
| 567 // Separator row height is the same size as itself. | |
| 568 if (m_popupClient->itemIsSeparator(index)) | |
| 569 return max(separatorHeight, minimumHeight); | |
| 570 | |
| 571 int fontHeight = getRowFont(index).fontMetrics().height(); | |
| 572 return max(fontHeight, minimumHeight); | |
| 573 } | |
| 574 | |
| 575 IntRect PopupListBox::getRowBounds(int index) | |
| 576 { | |
| 577 if (index < 0) | |
| 578 return IntRect(0, 0, visibleWidth(), getRowHeight(index)); | |
| 579 | |
| 580 return IntRect(0, m_items[index]->yOffset, visibleWidth(), getRowHeight(inde
x)); | |
| 581 } | |
| 582 | |
| 583 void PopupListBox::invalidateRow(int index) | |
| 584 { | |
| 585 if (index < 0) | |
| 586 return; | |
| 587 | |
| 588 // Invalidate in the window contents, as invalidateRect paints in the window
coordinates. | |
| 589 IntRect clipRect = contentsToWindow(getRowBounds(index)); | |
| 590 if (shouldPlaceVerticalScrollbarOnLeft() && verticalScrollbar() && !vertical
Scrollbar()->isOverlayScrollbar()) | |
| 591 clipRect.move(verticalScrollbar()->width(), 0); | |
| 592 invalidateRect(clipRect); | |
| 593 | |
| 594 if (HostWindow* host = hostWindow()) | |
| 595 host->invalidateDisplayItemClient(m_items[index]->displayItemClient()); | |
| 596 } | |
| 597 | |
| 598 void PopupListBox::scrollToRevealRow(int index) | |
| 599 { | |
| 600 if (index < 0) | |
| 601 return; | |
| 602 | |
| 603 IntRect rowRect = getRowBounds(index); | |
| 604 | |
| 605 if (rowRect.y() < scrollY()) { | |
| 606 // Row is above current scroll position, scroll up. | |
| 607 updateScrollbars(IntPoint(0, rowRect.y())); | |
| 608 } else if (rowRect.maxY() > scrollY() + visibleHeight()) { | |
| 609 // Row is below current scroll position, scroll down. | |
| 610 updateScrollbars(IntPoint(0, rowRect.maxY() - visibleHeight())); | |
| 611 } | |
| 612 } | |
| 613 | |
| 614 bool PopupListBox::isSelectableItem(int index) | |
| 615 { | |
| 616 ASSERT(index >= 0 && index < numItems()); | |
| 617 return m_items[index]->type == PopupItem::TypeOption && m_popupClient->itemI
sEnabled(index); | |
| 618 } | |
| 619 | |
| 620 void PopupListBox::clearSelection() | |
| 621 { | |
| 622 if (m_selectedIndex != -1) { | |
| 623 invalidateRow(m_selectedIndex); | |
| 624 m_selectedIndex = -1; | |
| 625 m_popupClient->selectionCleared(); | |
| 626 } | |
| 627 } | |
| 628 | |
| 629 void PopupListBox::selectNextRow() | |
| 630 { | |
| 631 adjustSelectedIndex(1); | |
| 632 } | |
| 633 | |
| 634 void PopupListBox::selectPreviousRow() | |
| 635 { | |
| 636 adjustSelectedIndex(-1); | |
| 637 } | |
| 638 | |
| 639 void PopupListBox::adjustSelectedIndex(int delta) | |
| 640 { | |
| 641 int targetIndex = m_selectedIndex + delta; | |
| 642 targetIndex = std::min(std::max(targetIndex, 0), numItems() - 1); | |
| 643 if (!isSelectableItem(targetIndex)) { | |
| 644 // We didn't land on an option. Try to find one. | |
| 645 // We try to select the closest index to target, prioritizing any in | |
| 646 // the range [current, target]. | |
| 647 | |
| 648 int dir = delta > 0 ? 1 : -1; | |
| 649 int testIndex = m_selectedIndex; | |
| 650 int bestIndex = m_selectedIndex; | |
| 651 bool passedTarget = false; | |
| 652 while (testIndex >= 0 && testIndex < numItems()) { | |
| 653 if (isSelectableItem(testIndex)) | |
| 654 bestIndex = testIndex; | |
| 655 if (testIndex == targetIndex) | |
| 656 passedTarget = true; | |
| 657 if (passedTarget && bestIndex != m_selectedIndex) | |
| 658 break; | |
| 659 | |
| 660 testIndex += dir; | |
| 661 } | |
| 662 | |
| 663 // Pick the best index, which may mean we don't change. | |
| 664 targetIndex = bestIndex; | |
| 665 } | |
| 666 | |
| 667 // Select the new index, and ensure its visible. We do this regardless of | |
| 668 // whether the selection changed to ensure keyboard events always bring the | |
| 669 // selection into view. | |
| 670 selectIndex(targetIndex); | |
| 671 scrollToRevealSelection(); | |
| 672 } | |
| 673 | |
| 674 void PopupListBox::hidePopup() | |
| 675 { | |
| 676 if (parent()) { | |
| 677 if (m_container->client()) | |
| 678 m_container->client()->popupClosed(m_container); | |
| 679 m_container->notifyPopupHidden(); | |
| 680 } | |
| 681 | |
| 682 if (m_popupClient) | |
| 683 m_popupClient->popupDidHide(); | |
| 684 } | |
| 685 | |
| 686 void PopupListBox::updateFromElement() | |
| 687 { | |
| 688 // Clearing the items doesn't immediately invalidate display items, but the | |
| 689 // layout at the end of this method does. | |
| 690 m_items.clear(); | |
| 691 | |
| 692 int size = m_popupClient->listSize(); | |
| 693 for (int i = 0; i < size; ++i) { | |
| 694 PopupItem::Type type; | |
| 695 if (m_popupClient->itemIsSeparator(i)) | |
| 696 type = PopupItem::TypeSeparator; | |
| 697 else if (m_popupClient->itemIsLabel(i)) | |
| 698 type = PopupItem::TypeGroup; | |
| 699 else | |
| 700 type = PopupItem::TypeOption; | |
| 701 m_items.append(adoptPtr(new PopupItem(m_popupClient->itemText(i), type))
); | |
| 702 m_items[i]->enabled = isSelectableItem(i); | |
| 703 PopupMenuStyle style = m_popupClient->itemStyle(i); | |
| 704 m_items[i]->textDirection = style.textDirection(); | |
| 705 m_items[i]->hasTextDirectionOverride = style.hasTextDirectionOverride(); | |
| 706 m_items[i]->displayNone = style.isDisplayNone(); | |
| 707 } | |
| 708 | |
| 709 if (m_selectedIndex >= static_cast<int>(m_items.size())) | |
| 710 m_selectedIndex = m_popupClient->selectedIndex(); | |
| 711 | |
| 712 layout(); | |
| 713 } | |
| 714 | |
| 715 void PopupListBox::setMaxWidthAndLayout(int maxWidth) | |
| 716 { | |
| 717 m_maxWindowWidth = maxWidth; | |
| 718 layout(); | |
| 719 } | |
| 720 | |
| 721 int PopupListBox::getRowBaseWidth(int index) | |
| 722 { | |
| 723 Font font = getRowFont(index); | |
| 724 PopupMenuStyle style = m_popupClient->itemStyle(index); | |
| 725 String text = m_popupClient->itemText(index); | |
| 726 if (text.isEmpty()) | |
| 727 return 0; | |
| 728 TextRun textRun(text, 0, 0, TextRun::AllowTrailingExpansion, style.textDirec
tion(), style.hasTextDirectionOverride()); | |
| 729 return font.width(textRun); | |
| 730 } | |
| 731 | |
| 732 void PopupListBox::layout() | |
| 733 { | |
| 734 bool isRightAligned = m_popupClient->menuStyle().textDirection() == RTL; | |
| 735 | |
| 736 // Size our child items. | |
| 737 int baseWidth = 0; | |
| 738 int paddingWidth = 0; | |
| 739 int lineEndPaddingWidth = 0; | |
| 740 int y = 0; | |
| 741 for (int i = 0; i < numItems(); ++i) { | |
| 742 // Place the item vertically. | |
| 743 m_items[i]->yOffset = y; | |
| 744 if (m_popupClient->itemStyle(i).isDisplayNone()) | |
| 745 continue; | |
| 746 y += getRowHeight(i); | |
| 747 | |
| 748 // Ensure the popup is wide enough to fit this item. | |
| 749 baseWidth = max(baseWidth, getRowBaseWidth(i)); | |
| 750 // FIXME: http://b/1210481 We should get the padding of individual | |
| 751 // option elements. | |
| 752 paddingWidth = max<int>(paddingWidth, | |
| 753 m_popupClient->clientPaddingLeft() + m_popupClient->clientPaddingRig
ht()); | |
| 754 lineEndPaddingWidth = max<int>(lineEndPaddingWidth, | |
| 755 isRightAligned ? m_popupClient->clientPaddingLeft() : m_popupClient-
>clientPaddingRight()); | |
| 756 } | |
| 757 | |
| 758 // Calculate scroll bar width. | |
| 759 int windowHeight = 0; | |
| 760 m_visibleRows = std::min(numItems(), maxVisibleRows); | |
| 761 | |
| 762 for (int i = 0; i < m_visibleRows; ++i) { | |
| 763 int rowHeight = getRowHeight(i); | |
| 764 | |
| 765 // Only clip the window height for non-Mac platforms. | |
| 766 if (windowHeight + rowHeight > m_maxHeight) { | |
| 767 m_visibleRows = i; | |
| 768 break; | |
| 769 } | |
| 770 | |
| 771 windowHeight += rowHeight; | |
| 772 } | |
| 773 | |
| 774 // Set our widget and scrollable contents sizes. | |
| 775 int scrollbarWidth = 0; | |
| 776 if (m_visibleRows < numItems()) { | |
| 777 if (!ScrollbarTheme::theme()->usesOverlayScrollbars()) | |
| 778 scrollbarWidth = ScrollbarTheme::theme()->scrollbarThickness(); | |
| 779 | |
| 780 // Use minEndOfLinePadding when there is a scrollbar so that we use | |
| 781 // as much as (lineEndPaddingWidth - minEndOfLinePadding) padding | |
| 782 // space for scrollbar and allow user to use CSS padding to make the | |
| 783 // popup listbox align with the select element. | |
| 784 paddingWidth = paddingWidth - lineEndPaddingWidth + minEndOfLinePadding; | |
| 785 } | |
| 786 | |
| 787 int windowWidth = baseWidth + scrollbarWidth + paddingWidth; | |
| 788 if (windowWidth > m_maxWindowWidth) { | |
| 789 // windowWidth exceeds m_maxWindowWidth, so we have to clip. | |
| 790 windowWidth = m_maxWindowWidth; | |
| 791 baseWidth = windowWidth - scrollbarWidth - paddingWidth; | |
| 792 m_baseWidth = baseWidth; | |
| 793 } | |
| 794 int contentWidth = windowWidth - scrollbarWidth; | |
| 795 | |
| 796 if (windowWidth < m_baseWidth) { | |
| 797 windowWidth = m_baseWidth; | |
| 798 contentWidth = m_baseWidth - scrollbarWidth; | |
| 799 } else { | |
| 800 m_baseWidth = baseWidth; | |
| 801 } | |
| 802 | |
| 803 resize(windowWidth, windowHeight); | |
| 804 setContentsSize(IntSize(contentWidth, getRowBounds(numItems() - 1).maxY())); | |
| 805 | |
| 806 if (hostWindow()) | |
| 807 scrollToRevealSelection(); | |
| 808 | |
| 809 invalidate(); | |
| 810 | |
| 811 // It's slight overkill to invalidate /all/ display items, but in practice | |
| 812 // most of the display items in the host window belong to the rows of this | |
| 813 // list box. | |
| 814 if (HostWindow* host = hostWindow()) | |
| 815 host->invalidateAllDisplayItems(); | |
| 816 } | |
| 817 | |
| 818 bool PopupListBox::isPointInBounds(const IntPoint& point) | |
| 819 { | |
| 820 return numItems() && IntRect(0, 0, width(), height()).contains(point); | |
| 821 } | |
| 822 | |
| 823 int PopupListBox::popupContentHeight() const | |
| 824 { | |
| 825 return height(); | |
| 826 } | |
| 827 | |
| 828 void PopupListBox::invalidateRect(const IntRect& rect) | |
| 829 { | |
| 830 if (HostWindow* h = hostWindow()) | |
| 831 h->invalidateRect(rect); | |
| 832 } | |
| 833 | |
| 834 IntRect PopupListBox::windowClipRect() const | |
| 835 { | |
| 836 IntRect clipRect = visibleContentRect(); | |
| 837 if (shouldPlaceVerticalScrollbarOnLeft() && verticalScrollbar() && !vertical
Scrollbar()->isOverlayScrollbar()) | |
| 838 clipRect.move(verticalScrollbar()->width(), 0); | |
| 839 return contentsToWindow(clipRect); | |
| 840 } | |
| 841 | |
| 842 void PopupListBox::invalidateScrollbarRect(Scrollbar* scrollbar, const IntRect&
rect) | |
| 843 { | |
| 844 // Add in our offset within the FrameView. | |
| 845 IntRect dirtyRect = rect; | |
| 846 dirtyRect.move(scrollbar->x(), scrollbar->y()); | |
| 847 invalidateRect(dirtyRect); | |
| 848 | |
| 849 if (HostWindow* host = hostWindow()) | |
| 850 host->invalidateDisplayItemClient(scrollbar->displayItemClient()); | |
| 851 } | |
| 852 | |
| 853 bool PopupListBox::isActive() const | |
| 854 { | |
| 855 // FIXME | |
| 856 return true; | |
| 857 } | |
| 858 | |
| 859 bool PopupListBox::scrollbarsCanBeActive() const | |
| 860 { | |
| 861 return isActive(); | |
| 862 } | |
| 863 | |
| 864 IntRect PopupListBox::scrollableAreaBoundingBox() const | |
| 865 { | |
| 866 return windowClipRect(); | |
| 867 } | |
| 868 | |
| 869 // FIXME: The following methods are based on code in FrameView, with | |
| 870 // simplifications for the constraints of PopupListBox (e.g. only vertical | |
| 871 // scrollbar, not horizontal). This functionality should be moved into | |
| 872 // ScrollableArea after http://crbug.com/417782 is fixed. | |
| 873 | |
| 874 void PopupListBox::setHasVerticalScrollbar(bool hasBar) | |
| 875 { | |
| 876 if (hasBar && !m_verticalScrollbar) { | |
| 877 m_verticalScrollbar = Scrollbar::create(this, VerticalScrollbar, Regular
Scrollbar); | |
| 878 m_verticalScrollbar->setParent(this); | |
| 879 didAddScrollbar(m_verticalScrollbar.get(), VerticalScrollbar); | |
| 880 m_verticalScrollbar->styleChanged(); | |
| 881 } else if (!hasBar && m_verticalScrollbar) { | |
| 882 m_verticalScrollbar->setParent(0); | |
| 883 willRemoveScrollbar(m_verticalScrollbar.get(), VerticalScrollbar); | |
| 884 m_verticalScrollbar = nullptr; | |
| 885 } | |
| 886 } | |
| 887 | |
| 888 Scrollbar* PopupListBox::scrollbarAtRootFramePoint(const IntPoint& pointInRootFr
ame) | |
| 889 { | |
| 890 return m_verticalScrollbar && m_verticalScrollbar->frameRect().contains( | |
| 891 convertFromContainingWindow(pointInRootFrame)) ? m_verticalScrollbar.get
() : 0; | |
| 892 } | |
| 893 | |
| 894 IntRect PopupListBox::contentsToWindow(const IntRect& contentsRect) const | |
| 895 { | |
| 896 IntRect viewRect = contentsRect; | |
| 897 viewRect.moveBy(-scrollPosition()); | |
| 898 return convertToContainingWindow(viewRect); | |
| 899 } | |
| 900 | |
| 901 void PopupListBox::setContentsSize(const IntSize& newSize) | |
| 902 { | |
| 903 if (contentsSize() == newSize) | |
| 904 return; | |
| 905 m_contentsSize = newSize; | |
| 906 updateScrollbars(scrollPosition()); | |
| 907 } | |
| 908 | |
| 909 void PopupListBox::setFrameRect(const IntRect& newRect) | |
| 910 { | |
| 911 IntRect oldRect = frameRect(); | |
| 912 if (newRect == oldRect) | |
| 913 return; | |
| 914 | |
| 915 Widget::setFrameRect(newRect); | |
| 916 updateScrollbars(scrollPosition()); | |
| 917 // NOTE: We do not need to call m_verticalScrollbar->frameRectsChanged as | |
| 918 // Scrollbar does not implement it. | |
| 919 } | |
| 920 | |
| 921 IntRect PopupListBox::visibleContentRect(IncludeScrollbarsInRect scrollbarInclus
ion) const | |
| 922 { | |
| 923 // NOTE: Unlike FrameView we do not need to incorporate any scaling factor, | |
| 924 // and there is only one scrollbar to exclude. | |
| 925 IntSize size = frameRect().size(); | |
| 926 Scrollbar* verticalBar = verticalScrollbar(); | |
| 927 if (scrollbarInclusion == ExcludeScrollbars && verticalBar && !verticalBar->
isOverlayScrollbar()) { | |
| 928 size.setWidth(std::max(0, size.width() - verticalBar->width())); | |
| 929 } | |
| 930 return IntRect(m_scrollOffset, size); | |
| 931 } | |
| 932 | |
| 933 void PopupListBox::updateScrollbars(IntPoint desiredOffset) | |
| 934 { | |
| 935 IntSize oldVisibleSize = visibleContentRect().size(); | |
| 936 adjustScrollbarExistence(); | |
| 937 updateScrollbarGeometry(); | |
| 938 IntSize newVisibleSize = visibleContentRect().size(); | |
| 939 | |
| 940 if (newVisibleSize.width() > oldVisibleSize.width()) { | |
| 941 if (shouldPlaceVerticalScrollbarOnLeft()) | |
| 942 invalidateRect(IntRect(0, 0, newVisibleSize.width() - oldVisibleSize
.width(), newVisibleSize.height())); | |
| 943 else | |
| 944 invalidateRect(IntRect(oldVisibleSize.width(), 0, newVisibleSize.wid
th() - oldVisibleSize.width(), newVisibleSize.height())); | |
| 945 } | |
| 946 | |
| 947 desiredOffset = desiredOffset.shrunkTo(maximumScrollPosition()); | |
| 948 desiredOffset = desiredOffset.expandedTo(minimumScrollPosition()); | |
| 949 | |
| 950 if (desiredOffset != scrollPosition()) | |
| 951 ScrollableArea::scrollToOffsetWithoutAnimation(desiredOffset); | |
| 952 } | |
| 953 | |
| 954 void PopupListBox::adjustScrollbarExistence() | |
| 955 { | |
| 956 bool needsVerticalScrollbar = contentsSize().height() > visibleHeight(); | |
| 957 if (!!m_verticalScrollbar != needsVerticalScrollbar) { | |
| 958 setHasVerticalScrollbar(needsVerticalScrollbar); | |
| 959 contentsResized(); | |
| 960 } | |
| 961 } | |
| 962 | |
| 963 void PopupListBox::updateScrollbarGeometry() | |
| 964 { | |
| 965 if (m_verticalScrollbar) { | |
| 966 int clientHeight = visibleHeight(); | |
| 967 IntRect oldRect(m_verticalScrollbar->frameRect()); | |
| 968 IntRect vBarRect(shouldPlaceVerticalScrollbarOnLeft() ? 0 : (width() - m
_verticalScrollbar->width()), | |
| 969 0, m_verticalScrollbar->width(), height()); | |
| 970 m_verticalScrollbar->setFrameRect(vBarRect); | |
| 971 if (oldRect != m_verticalScrollbar->frameRect()) | |
| 972 m_verticalScrollbar->invalidate(); | |
| 973 | |
| 974 m_verticalScrollbar->setEnabled(contentsSize().height() > clientHeight); | |
| 975 m_verticalScrollbar->setProportion(clientHeight, contentsSize().height()
); | |
| 976 m_verticalScrollbar->offsetDidChange(); | |
| 977 // NOTE: PopupListBox does not support suppressing scrollbars. | |
| 978 } | |
| 979 } | |
| 980 | |
| 981 IntPoint PopupListBox::convertChildToSelf(const Widget* child, const IntPoint& p
oint) const | |
| 982 { | |
| 983 // NOTE: m_verticalScrollbar is the only child. | |
| 984 IntPoint newPoint = point; | |
| 985 newPoint.moveBy(child->location()); | |
| 986 return newPoint; | |
| 987 } | |
| 988 | |
| 989 IntPoint PopupListBox::convertSelfToChild(const Widget* child, const IntPoint& p
oint) const | |
| 990 { | |
| 991 // NOTE: m_verticalScrollbar is the only child. | |
| 992 IntPoint newPoint = point; | |
| 993 newPoint.moveBy(-child->location()); | |
| 994 return newPoint; | |
| 995 } | |
| 996 | |
| 997 int PopupListBox::scrollSize(ScrollbarOrientation orientation) const | |
| 998 { | |
| 999 return (orientation == HorizontalScrollbar || !m_verticalScrollbar) ? | |
| 1000 0 : m_verticalScrollbar->totalSize() - m_verticalScrollbar->visibleSize(
); | |
| 1001 } | |
| 1002 | |
| 1003 void PopupListBox::setScrollOffset(const IntPoint& newOffset) | |
| 1004 { | |
| 1005 // NOTE: We do not support any "fast path" for scrolling. When the scroll | |
| 1006 // offset changes, we just repaint the whole popup. | |
| 1007 IntSize scrollDelta = newOffset - m_scrollOffset; | |
| 1008 if (scrollDelta == IntSize()) | |
| 1009 return; | |
| 1010 m_scrollOffset = newOffset; | |
| 1011 | |
| 1012 if (HostWindow* window = hostWindow()) { | |
| 1013 IntRect clipRect = windowClipRect(); | |
| 1014 IntRect updateRect = clipRect; | |
| 1015 updateRect.intersect(convertToContainingWindow(IntRect((shouldPlaceVerti
calScrollbarOnLeft() && verticalScrollbar()) ? verticalScrollbar()->width() : 0,
0, visibleWidth(), visibleHeight()))); | |
| 1016 window->invalidateRect(updateRect); | |
| 1017 window->invalidateDisplayItemClient(displayItemClient()); | |
| 1018 } | |
| 1019 } | |
| 1020 | |
| 1021 IntPoint PopupListBox::maximumScrollPosition() const | |
| 1022 { | |
| 1023 IntPoint maximumOffset(contentsSize().width() - visibleWidth() - scrollOrigi
n().x(), contentsSize().height() - visibleHeight() - scrollOrigin().y()); | |
| 1024 maximumOffset.clampNegativeToZero(); | |
| 1025 return maximumOffset; | |
| 1026 } | |
| 1027 | |
| 1028 IntPoint PopupListBox::minimumScrollPosition() const | |
| 1029 { | |
| 1030 return IntPoint(-scrollOrigin().x(), -scrollOrigin().y()); | |
| 1031 } | |
| 1032 | |
| 1033 } // namespace blink | |
| OLD | NEW |