WebCore:
[WebKit-https.git] / WebCore / rendering / RenderListBox.cpp
1 /*
2  * This file is part of the select element renderer in WebCore.
3  *
4  * Copyright (C) 2006, 2007 Apple Inc. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *
10  * 1.  Redistributions of source code must retain the above copyright
11  *     notice, this list of conditions and the following disclaimer. 
12  * 2.  Redistributions in binary form must reproduce the above copyright
13  *     notice, this list of conditions and the following disclaimer in the
14  *     documentation and/or other materials provided with the distribution. 
15  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16  *     its contributors may be used to endorse or promote products derived
17  *     from this software without specific prior written permission. 
18  *
19  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 #include "config.h"
32 #include "RenderListBox.h"
33
34 #include "Document.h"
35 #include "EventHandler.h"
36 #include "EventNames.h"
37 #include "Frame.h"
38 #include "FrameView.h"
39 #include "GraphicsContext.h"
40 #include "HTMLNames.h"
41 #include "HTMLOptGroupElement.h"
42 #include "HTMLOptionElement.h"
43 #include "HTMLSelectElement.h"
44 #include "HitTestResult.h"
45 #include "PlatformScrollBar.h" 
46 #include "RenderTheme.h"
47 #include "RenderView.h"
48 #include "TextStyle.h"
49 #include <math.h>
50
51 using namespace std;
52
53 namespace WebCore {
54
55 using namespace EventNames;
56 using namespace HTMLNames;
57  
58 const int rowSpacing = 1;
59
60 const int optionsSpacingHorizontal = 2;
61
62 const int minSize = 4;
63 const int maxDefaultSize = 10;
64
65 // FIXME: This hardcoded baselineAdjustment is what we used to do for the old
66 // widget, but I'm not sure this is right for the new control.
67 const int baselineAdjustment = 7;
68
69 RenderListBox::RenderListBox(HTMLSelectElement* element)
70     : RenderBlock(element)
71     , m_optionsChanged(true)
72     , m_scrollToRevealSelectionAfterLayout(false)
73     , m_inAutoscroll(false)
74     , m_optionsWidth(0)
75     , m_indexOffset(0)
76 {
77 }
78
79 RenderListBox::~RenderListBox()
80 {
81     if (m_vBar && m_vBar->isWidget())
82         if (FrameView* view = node()->document()->view())
83             view->removeChild(static_cast<PlatformScrollbar*>(m_vBar.get()));
84 }
85
86 void RenderListBox::setStyle(RenderStyle* style)
87 {
88     RenderBlock::setStyle(style);
89     setReplaced(isInline());
90 }
91
92 void RenderListBox::updateFromElement()
93 {
94     if (m_optionsChanged) {
95         const Vector<HTMLElement*>& listItems = static_cast<HTMLSelectElement*>(node())->listItems();
96         int size = numItems();
97         
98         float width = 0;
99         TextStyle textStyle(0, 0, 0, false, false, false, false);
100         for (int i = 0; i < size; ++i) {
101             HTMLElement* element = listItems[i];
102             String text;
103             Font itemFont = style()->font();
104             if (element->hasTagName(optionTag))
105                 text = static_cast<HTMLOptionElement*>(element)->optionText();
106             else if (element->hasTagName(optgroupTag)) {
107                 text = static_cast<HTMLOptGroupElement*>(element)->groupLabelText();
108                 FontDescription d = itemFont.fontDescription();
109                 d.setBold(true);
110                 itemFont = Font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
111                 itemFont.update();
112             }
113                 
114             if (!text.isEmpty()) {
115                 float textWidth = itemFont.floatWidth(TextRun(text.impl()), textStyle);
116                 width = max(width, textWidth);
117             }
118         }
119         m_optionsWidth = static_cast<int>(ceilf(width));
120         m_optionsChanged = false;
121         setNeedsLayoutAndMinMaxRecalc();
122     }
123 }
124
125 void RenderListBox::selectionChanged()
126 {
127     repaint();
128     if (!m_inAutoscroll) {
129         if (needsLayout())
130             m_scrollToRevealSelectionAfterLayout = true;
131         else
132             scrollToRevealSelection();
133     }
134 }
135
136 void RenderListBox::layout()
137 {
138     RenderBlock::layout();
139     if (m_scrollToRevealSelectionAfterLayout)
140         scrollToRevealSelection();
141 }
142
143 void RenderListBox::scrollToRevealSelection()
144 {    
145     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
146
147     m_scrollToRevealSelectionAfterLayout = false;
148
149     int firstIndex = select->activeSelectionStartListIndex();
150     if (firstIndex >= 0 && !listIndexIsVisible(select->activeSelectionEndListIndex()))
151         scrollToRevealElementAtListIndex(firstIndex);
152 }
153
154 void RenderListBox::calcMinMaxWidth()
155 {
156     if (!m_vBar && Scrollbar::hasPlatformScrollbars())
157         if (FrameView* view = node()->document()->view()) {
158             RefPtr<PlatformScrollbar> widget = new PlatformScrollbar(this, VerticalScrollbar, SmallScrollbar);
159             view->addChild(widget.get());
160             m_vBar = widget.release();
161         }
162
163     if (m_optionsChanged)
164         updateFromElement();
165
166     m_minWidth = 0;
167     m_maxWidth = 0;
168
169     if (style()->width().isFixed() && style()->width().value() > 0)
170         m_minWidth = m_maxWidth = calcContentBoxWidth(style()->width().value());
171     else {
172         m_maxWidth = m_optionsWidth + 2 * optionsSpacingHorizontal;
173         if (m_vBar)
174             m_maxWidth += m_vBar->width();
175     }
176
177     if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) {
178         m_maxWidth = max(m_maxWidth, calcContentBoxWidth(style()->minWidth().value()));
179         m_minWidth = max(m_minWidth, calcContentBoxWidth(style()->minWidth().value()));
180     } else if (style()->width().isPercent() || (style()->width().isAuto() && style()->height().isPercent()))
181         m_minWidth = 0;
182     else
183         m_minWidth = m_maxWidth;
184
185     if (style()->maxWidth().isFixed() && style()->maxWidth().value() != undefinedLength) {
186         m_maxWidth = min(m_maxWidth, calcContentBoxWidth(style()->maxWidth().value()));
187         m_minWidth = min(m_minWidth, calcContentBoxWidth(style()->maxWidth().value()));
188     }
189
190     int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight();
191     m_minWidth += toAdd;
192     m_maxWidth += toAdd;
193                                 
194     setMinMaxKnown();
195 }
196
197 int RenderListBox::size() const
198 {
199     int specifiedSize = static_cast<HTMLSelectElement*>(node())->size();
200     if (specifiedSize > 1)
201         return max(minSize, specifiedSize);
202     return min(max(minSize, numItems()), maxDefaultSize);
203 }
204
205 int RenderListBox::numVisibleItems() const
206 {
207     // Only count fully visible rows. But don't return 0 even if only part of a row shows.
208     return max(1, (contentHeight() + rowSpacing) / itemHeight());
209 }
210
211 int RenderListBox::numItems() const
212 {
213     return static_cast<HTMLSelectElement*>(node())->listItems().size();
214 }
215
216 int RenderListBox::listHeight() const
217 {
218     return itemHeight() * numItems() - rowSpacing;
219 }
220
221 void RenderListBox::calcHeight()
222 {
223     int toAdd = paddingTop() + paddingBottom() + borderTop() + borderBottom();
224  
225     int itemHeight = RenderListBox::itemHeight();
226     m_height = itemHeight * size() - rowSpacing + toAdd;
227     
228     RenderBlock::calcHeight();
229     
230     if (m_vBar) {
231         m_vBar->setEnabled(numVisibleItems() < numItems());
232         m_vBar->setSteps(1, min(1, numVisibleItems() - 1), itemHeight);
233         m_vBar->setProportion(numVisibleItems(), numItems());
234     }
235 }
236
237 short RenderListBox::baselinePosition(bool b, bool isRootLineBox) const
238 {
239     return height() + marginTop() + marginBottom() - baselineAdjustment;
240 }
241
242 IntRect RenderListBox::itemBoundingBoxRect(int tx, int ty, int index)
243 {
244     return IntRect(tx + borderLeft() + paddingLeft(),
245                    ty + borderTop() + paddingTop() + itemHeight() * (index - m_indexOffset),
246                    contentWidth(), itemHeight());
247 }
248     
249 void RenderListBox::paintObject(PaintInfo& paintInfo, int tx, int ty)
250 {
251     int listItemsSize = numItems();
252
253     if (paintInfo.phase == PaintPhaseForeground) {
254         int index = m_indexOffset;
255         while (index < listItemsSize && index <= m_indexOffset + numVisibleItems()) {
256             paintItemForeground(paintInfo, tx, ty, index);
257             index++;
258         }
259     }
260
261     // Paint the children.
262     RenderBlock::paintObject(paintInfo, tx, ty);
263
264     if (paintInfo.phase == PaintPhaseChildBlockBackground || paintInfo.phase == PaintPhaseChildBlockBackgrounds) {
265         int index = m_indexOffset;
266         while (index < listItemsSize && index <= m_indexOffset + numVisibleItems()) {
267             paintItemBackground(paintInfo, tx, ty, index);
268             index++;
269         }
270         paintScrollbar(paintInfo);
271     }
272 }
273
274 void RenderListBox::paintScrollbar(PaintInfo& paintInfo)
275 {
276     if (m_vBar) {
277         IntRect absBounds = absoluteBoundingBoxRect();
278         IntRect scrollRect(absBounds.right() - borderRight() - m_vBar->width(),
279                            absBounds.y() + borderTop(),
280                            m_vBar->width(),
281                            absBounds.height() - (borderTop() + borderBottom()));
282         m_vBar->setRect(scrollRect);
283         m_vBar->paint(paintInfo.context, scrollRect);
284     }
285 }
286
287 void RenderListBox::paintItemForeground(PaintInfo& paintInfo, int tx, int ty, int listIndex)
288 {
289     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
290     const Vector<HTMLElement*>& listItems = select->listItems();
291     HTMLElement* element = listItems[listIndex];
292
293     String itemText;
294     if (element->hasTagName(optionTag))
295         itemText = static_cast<HTMLOptionElement*>(element)->optionText();
296     else if (element->hasTagName(optgroupTag))
297         itemText = static_cast<HTMLOptGroupElement*>(element)->groupLabelText();
298        
299     // Determine where the item text should be placed
300     IntRect r = itemBoundingBoxRect(tx, ty, listIndex);
301     r.move(optionsSpacingHorizontal, style()->font().ascent());
302
303     RenderStyle* itemStyle = element->renderStyle();
304     if (!itemStyle)
305         itemStyle = style();
306     
307     Color textColor = element->renderStyle() ? element->renderStyle()->color() : style()->color();
308     if (element->hasTagName(optionTag) && static_cast<HTMLOptionElement*>(element)->selected()) {
309         if (document()->frame()->isActive() && document()->focusedNode() == node())
310             textColor = theme()->activeListBoxSelectionForegroundColor();
311         // Honor the foreground color for disabled items
312         else if (!element->disabled())
313             textColor = theme()->inactiveListBoxSelectionForegroundColor();
314     }
315
316     paintInfo.context->setFillColor(textColor);
317
318     Font itemFont = style()->font();
319     if (element->hasTagName(optgroupTag)) {
320         FontDescription d = itemFont.fontDescription();
321         d.setBold(true);
322         itemFont = Font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
323         itemFont.update();
324     }
325     paintInfo.context->setFont(itemFont);
326     
327     unsigned length = itemText.length();
328     const UChar* string = itemText.characters();
329     TextStyle textStyle(0, 0, 0, false, true);
330     RenderBlock::CharacterBuffer characterBuffer;
331
332     if (itemStyle->direction() == RTL && itemStyle->unicodeBidi() == Override)
333         textStyle.setRTL(true);
334     else if ((itemStyle->direction() == RTL || itemStyle->unicodeBidi() != Override) && !itemStyle->visuallyOrdered()) {
335         // If necessary, reorder characters by running the string through the bidi algorithm
336         characterBuffer.append(string, length);
337         RenderBlock::bidiReorderCharacters(document(), itemStyle, characterBuffer);
338         string = characterBuffer.data();
339     }
340     TextRun textRun(string, length);
341
342     // Draw the item text
343     if (itemStyle->visibility() != HIDDEN)
344         paintInfo.context->drawText(textRun, r.location(), textStyle);
345 }
346
347 void RenderListBox::paintItemBackground(PaintInfo& paintInfo, int tx, int ty, int listIndex)
348 {
349     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
350     const Vector<HTMLElement*>& listItems = select->listItems();
351     HTMLElement* element = listItems[listIndex];
352
353     Color backColor;
354     if (element->hasTagName(optionTag) && static_cast<HTMLOptionElement*>(element)->selected()) {
355         if (document()->frame()->isActive() && document()->focusedNode() == node())
356             backColor = theme()->activeListBoxSelectionBackgroundColor();
357         else
358             backColor = theme()->inactiveListBoxSelectionBackgroundColor();
359     } else
360         backColor = element->renderStyle() ? element->renderStyle()->backgroundColor() : style()->backgroundColor();
361
362     // Draw the background for this list box item
363     if (!element->renderStyle() || element->renderStyle()->visibility() != HIDDEN) {
364         IntRect itemRect = itemBoundingBoxRect(tx, ty, listIndex);
365         itemRect.intersect(controlClipRect(tx, ty));
366         paintInfo.context->fillRect(itemRect, backColor);
367     }
368 }
369
370 bool RenderListBox::isPointInScrollbar(HitTestResult& result, int _x, int _y, int _tx, int _ty)
371 {
372     if (!m_vBar)
373         return false;
374
375     IntRect vertRect(_tx + width() - borderRight() - m_vBar->width(),
376                    _ty + borderTop() - borderTopExtra(),
377                    m_vBar->width(),
378                    height() + borderTopExtra() + borderBottomExtra() - borderTop() - borderBottom());
379
380     if (vertRect.contains(_x, _y)) {
381         result.setScrollbar(m_vBar->isWidget() ? static_cast<PlatformScrollbar*>(m_vBar.get()) : 0);
382         return true;
383     }
384     return false;
385 }
386
387 int RenderListBox::listIndexAtOffset(int offsetX, int offsetY)
388 {
389     if (!numItems())
390         return -1;
391
392     if (offsetY < borderTop() + paddingTop() || offsetY > height() - paddingBottom() - borderBottom())
393         return -1;
394
395     int scrollbarWidth = m_vBar ? m_vBar->width() : 0;
396     if (offsetX < borderLeft() + paddingLeft() || offsetX > width() - borderRight() - paddingRight() - scrollbarWidth)
397         return -1;
398
399     int newOffset = (offsetY - borderTop() - paddingTop()) / itemHeight() + m_indexOffset;
400     return newOffset < numItems() ? newOffset : -1;
401 }
402
403 void RenderListBox::autoscroll()
404 {
405     IntPoint pos = document()->frame()->view()->windowToContents(document()->frame()->eventHandler()->currentMousePosition());
406
407     int rx = 0;
408     int ry = 0;
409     absolutePosition(rx, ry);
410     int offsetX = pos.x() - rx;
411     int offsetY = pos.y() - ry;
412     
413     int endIndex = -1;
414     int rows = numVisibleItems();
415     int offset = m_indexOffset;
416     if (offsetY < borderTop() + paddingTop() && scrollToRevealElementAtListIndex(offset - 1))
417         endIndex = offset - 1;
418     else if (offsetY > height() - paddingBottom() - borderBottom() && scrollToRevealElementAtListIndex(offset + rows))
419         endIndex = offset + rows - 1;
420     else
421         endIndex = listIndexAtOffset(offsetX, offsetY);
422
423     if (endIndex >= 0) {
424         HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
425         m_inAutoscroll = true;
426         if (!select->multiple())
427             select->setActiveSelectionAnchorIndex(endIndex);
428         select->setActiveSelectionEndIndex(endIndex);
429         select->updateListBoxSelection(!select->multiple());
430         m_inAutoscroll = false;
431     }
432 }
433
434 void RenderListBox::stopAutoscroll()
435 {
436     static_cast<HTMLSelectElement*>(node())->listBoxOnChange();
437 }
438
439 bool RenderListBox::scrollToRevealElementAtListIndex(int index)
440 {
441     if (index < 0 || index >= numItems() || listIndexIsVisible(index))
442         return false;
443
444     int newOffset;
445     if (index < m_indexOffset)
446         newOffset = index;
447     else
448         newOffset = index - numVisibleItems() + 1;
449
450     m_indexOffset = newOffset;
451     if (m_vBar)
452         m_vBar->setValue(m_indexOffset);
453
454     return true;
455 }
456
457 bool RenderListBox::listIndexIsVisible(int index)
458 {    
459     return index >= m_indexOffset && index < m_indexOffset + numVisibleItems();
460 }
461
462 bool RenderListBox::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier)
463 {
464     return m_vBar && m_vBar->scroll(direction, granularity, multiplier);
465 }
466
467 void RenderListBox::valueChanged(unsigned listIndex)
468 {
469     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
470     select->setSelectedIndex(select->listToOptionIndex(listIndex));
471     select->onChange();
472 }
473
474 void RenderListBox::valueChanged(Scrollbar*)
475 {
476     int newOffset = m_vBar->value();
477     if (newOffset != m_indexOffset) {
478         m_indexOffset = newOffset;
479         repaint();
480         // Fire the scroll DOM event.
481         EventTargetNodeCast(node())->dispatchHTMLEvent(scrollEvent, true, false);
482     }
483 }
484
485 int RenderListBox::itemHeight() const
486 {
487     return style()->font().height() + rowSpacing;
488 }
489
490 int RenderListBox::verticalScrollbarWidth() const
491 {
492     return m_vBar ? m_vBar->width() : 0;
493 }
494
495 // FIXME: We ignore padding in the vertical direction as far as these values are concerned, since that's
496 // how the control currently paints.
497 int RenderListBox::scrollWidth() const
498 {
499     // There is no horizontal scrolling allowed.
500     return clientWidth();
501 }
502
503 int RenderListBox::scrollHeight() const
504 {
505     return max(clientHeight(), listHeight());
506 }
507
508 int RenderListBox::scrollLeft() const
509 {
510     return 0;
511 }
512
513 void RenderListBox::setScrollLeft(int)
514 {
515 }
516
517 int RenderListBox::scrollTop() const
518 {
519     return m_indexOffset * itemHeight();
520 }
521
522 void RenderListBox::setScrollTop(int newTop)
523 {
524     // Determine an index and scroll to it.    
525     int index = newTop / itemHeight();
526     if (index < 0 || index >= numItems() || index == m_indexOffset)
527         return;
528     m_indexOffset = index;
529     if (m_vBar)
530         m_vBar->setValue(index);
531 }
532
533 IntRect RenderListBox::controlClipRect(int tx, int ty) const
534 {
535     IntRect clipRect = contentBox();
536     clipRect.move(tx, ty);
537     return clipRect;
538 }
539
540 IntRect RenderListBox::windowClipRect() const
541 {
542     FrameView* frameView = view()->frameView();
543     if (!frameView)
544         return IntRect();
545
546     return frameView->windowClipRectForLayer(enclosingLayer(), true);
547 }
548
549 bool RenderListBox::isScrollable() const
550 {
551     if (numVisibleItems() < numItems())
552         return true;
553     return RenderObject::isScrollable();
554 }
555
556 } // namespace WebCore