897409109a836dd1818d05601ce00399a865f513
[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 Apple Computer, Inc.
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Library General Public
8  * License as published by the Free Software Foundation; either
9  * version 2 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Library General Public License for more details.
15  *
16  * You should have received a copy of the GNU Library General Public License
17  * along with this library; see the file COPYING.LIB.  If not, write to
18  * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19  * Boston, MA 02111-1307, USA.
20  *
21  */
22
23 #include "config.h"
24 #include "RenderListBox.h"
25
26 #include "Document.h"
27 #include "EventNames.h"
28 #include "Frame.h"
29 #include "FrameView.h"
30 #include "GraphicsContext.h"
31 #include "HTMLNames.h"
32 #include "HTMLOptionElement.h"
33 #include "HTMLOptGroupElement.h"
34 #include "HTMLSelectElement.h"
35 #include "PlatformScrollBar.h" 
36 #include "RenderBR.h"
37 #include "RenderText.h"
38 #include "RenderTheme.h"
39 #include "TextStyle.h"
40 #include <math.h>
41
42 using namespace std;
43
44 namespace WebCore {
45
46 using namespace HTMLNames;
47 using namespace EventNames;
48  
49 const int optionsSpacingMiddle = 1;
50 const int optionsSpacingLeft = 2;
51
52 RenderListBox::RenderListBox(HTMLSelectElement* element)
53     : RenderBlock(element)
54     , m_optionsChanged(true)
55     , m_optionsWidth(0)
56     , m_indexOffset(0)
57     , m_selectionChanged(true)
58     , m_vBar(0)
59 {
60 }
61
62 RenderListBox::~RenderListBox()
63 {
64     // If this list box is being autoscrolled, stop the autoscroll timer
65     if (document() && document()->frame() && document()->frame()->autoscrollRenderer() == this)
66         document()->frame()->stopAutoscrollTimer();
67
68     if (m_vBar && m_vBar->isWidget()) {
69         element()->document()->view()->removeChild(static_cast<PlatformScrollbar*>(m_vBar));
70         m_vBar->deref();
71     }
72 }
73
74
75 void RenderListBox::setStyle(RenderStyle* style)
76 {
77     RenderBlock::setStyle(style);
78     setReplaced(isInline());
79 }
80
81 void RenderListBox::updateFromElement()
82 {
83     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
84     const Vector<HTMLElement*>& listItems = select->listItems();
85     int size = listItems.size();
86     if (m_optionsChanged) {  
87         float width = 0;
88         TextStyle textStyle(0, 0, 0, false, false, false, false);
89         for (int i = 0; i < size; ++i) {
90             HTMLElement* element = listItems[i];
91             String text;
92             Font itemFont = style()->font();
93             if (element->hasTagName(optionTag))
94                 text = static_cast<HTMLOptionElement*>(element)->optionText();
95             else if (element->hasTagName(optgroupTag)) {
96                 text = static_cast<HTMLOptGroupElement*>(element)->groupLabelText();
97                 FontDescription d = itemFont.fontDescription();
98                 d.setBold(true);
99                 itemFont = Font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
100                 itemFont.update();
101             }
102                 
103             if (!text.isEmpty()) {
104                 float textWidth = itemFont.floatWidth(TextRun(text.impl()), textStyle);
105                 width = max(width, textWidth);
106             }
107         }
108         m_optionsWidth = static_cast<int>(ceilf(width));
109         m_optionsChanged = false;
110     }
111 }
112
113 void RenderListBox::calcMinMaxWidth()
114 {
115     if (!m_vBar) {
116         if (Scrollbar::hasPlatformScrollbars()) {
117             PlatformScrollbar* widget = new PlatformScrollbar(this, VerticalScrollbar, SmallScrollbar);
118             widget->ref();
119             node()->document()->view()->addChild(widget);
120             m_vBar = widget;
121         }
122     }
123     if (m_optionsChanged)
124         updateFromElement();
125
126     m_minWidth = 0;
127     m_maxWidth = 0;
128
129     if (style()->width().isFixed() && style()->width().value() > 0)
130         m_minWidth = m_maxWidth = calcContentBoxWidth(style()->width().value());
131     else {
132         m_maxWidth = m_optionsWidth;
133         if (m_vBar)
134            m_maxWidth += m_vBar->width();
135     }
136
137     if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) {
138         m_maxWidth = max(m_maxWidth, calcContentBoxWidth(style()->minWidth().value()));
139         m_minWidth = max(m_minWidth, calcContentBoxWidth(style()->minWidth().value()));
140     } else if (style()->width().isPercent() || (style()->width().isAuto() && style()->height().isPercent()))
141         m_minWidth = 0;
142     else
143         m_minWidth = m_maxWidth;
144
145     if (style()->maxWidth().isFixed() && style()->maxWidth().value() != undefinedLength) {
146         m_maxWidth = min(m_maxWidth, calcContentBoxWidth(style()->maxWidth().value()));
147         m_minWidth = min(m_minWidth, calcContentBoxWidth(style()->maxWidth().value()));
148     }
149
150     int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight();
151     m_minWidth += toAdd;
152     m_maxWidth += toAdd;
153                                 
154     setMinMaxKnown();
155 }
156
157 const int minSize = 4;
158 const int minDefaultSize = 10;
159 int RenderListBox::size() const
160 {
161     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
162     int specifiedSize = select->size();
163     if (specifiedSize > 1)
164         return max(minSize, specifiedSize);
165
166     return min(max(numItems(), minSize), minDefaultSize);
167 }
168
169 int RenderListBox::numItems() const
170 {
171     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
172     return select->listItems().size();
173 }
174
175 void RenderListBox::calcHeight()
176 {
177     int toAdd = paddingTop() + paddingBottom() + borderTop() + borderBottom();
178
179     int itemHeight = style()->font().height() + optionsSpacingMiddle;
180     m_height = itemHeight * size() + toAdd;
181     
182     RenderBlock::calcHeight();
183     
184     if (m_vBar) {
185         m_vBar->setEnabled(size() < numItems());
186         m_vBar->setSteps(itemHeight, itemHeight);
187         m_vBar->setProportion(m_height - toAdd, itemHeight * numItems());
188     }
189 }
190
191 const int baselineAdjustment = 7;
192 short RenderListBox::baselinePosition(bool b, bool isRootLineBox) const
193 {
194     // FIXME: This hardcoded baselineAdjustment is what we used to do for the old widget, but I'm not sure this is right for the new control.
195     return height() + marginTop() + marginBottom() - baselineAdjustment;
196 }
197
198 IntRect RenderListBox::itemBoundingBoxRect(int tx, int ty, int index)
199 {
200     return IntRect (tx + borderLeft() + paddingLeft(),
201                    ty + borderTop() + paddingTop() + ((style()->font().height() + optionsSpacingMiddle) * (index - m_indexOffset)),
202                    absoluteBoundingBoxRect().width() - borderLeft() - borderRight() - paddingLeft() - paddingRight(),
203                    style()->font().height() + optionsSpacingMiddle);
204 }
205     
206 void RenderListBox::paintObject(PaintInfo& i, int tx, int ty)
207 {
208     // Push a clip.
209     IntRect clipRect(tx + borderLeft(), ty + borderTop(),
210          width() - borderLeft() - borderRight(), height() - borderBottom() - borderTop());
211     if (i.phase == PaintPhaseForeground || i.phase == PaintPhaseChildBlockBackgrounds) {
212         if (clipRect.width() == 0 || clipRect.height() == 0)
213             return;
214         i.p->save();
215         i.p->clip(clipRect);
216     }
217     
218     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
219     int listItemsSize = select->listItems().size();
220
221     if (i.phase == PaintPhaseForeground) {
222         int index = m_indexOffset;
223         while (index < listItemsSize && index < m_indexOffset + size()) {
224             paintItemForeground(i, tx, ty, index);
225             index++;
226         }
227     }
228     
229     // Paint the children.
230     RenderBlock::paintObject(i, tx, ty);
231     
232     if (i.phase == PaintPhaseBlockBackground) {
233         int index = m_indexOffset;
234         while (index < listItemsSize && index < m_indexOffset + size()) {
235             paintItemBackground(i, tx, ty, index);
236             index++;
237         }
238         paintScrollbar(i);
239     }
240     // Pop the clip.
241     if (i.phase == PaintPhaseForeground || i.phase == PaintPhaseChildBlockBackgrounds)
242         i.p->restore();
243 }
244
245 void RenderListBox::paintScrollbar(PaintInfo& i)
246 {
247     if (m_vBar) {
248         IntRect absBounds = absoluteBoundingBoxRect();
249         IntRect scrollRect(absBounds.right() - borderRight() - m_vBar->width(),
250                 absBounds.y() + borderTop(),
251                 m_vBar->width(),
252                 absBounds.height() - (borderTop() + borderBottom()));
253         m_vBar->setRect(scrollRect);
254         m_vBar->paint(i.p, scrollRect);
255     }
256 }
257
258 void RenderListBox::paintItemForeground(PaintInfo& i, int tx, int ty, int listIndex)
259 {
260     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
261     const Vector<HTMLElement*>& listItems = select->listItems();
262     HTMLElement* element = listItems[listIndex];
263             
264     String itemText;
265     if (element->hasTagName(optionTag))
266         itemText = static_cast<HTMLOptionElement*>(element)->optionText();
267     else if (element->hasTagName(optgroupTag))
268         itemText = static_cast<HTMLOptGroupElement*>(element)->groupLabelText();
269    
270     TextRun textRun(itemText.characters(), itemText.length());
271     
272     // Determine where the item text should be placed
273     IntRect r = itemBoundingBoxRect(tx, ty, listIndex);
274     r.move(optionsSpacingLeft, style()->font().ascent());
275
276     RenderStyle* itemStyle = element->renderStyle();
277     if (!itemStyle)
278         itemStyle = style();
279     
280     Color textColor = element->renderStyle() ? element->renderStyle()->color() : style()->color();
281     if (element->hasTagName(optionTag) && static_cast<HTMLOptionElement*>(element)->selected()) {
282         if (document()->frame()->isActive() && document()->focusNode() == node())
283             textColor = theme()->activeListBoxSelectionForegroundColor();
284  /*
285     FIXME: Decide what the desired behavior is for inactive foreground color.  
286     For example, disabled items have a dark grey foreground color defined in CSS.  
287     If we don't honor that, the selected disabled items will have a weird black-on-grey look.
288         else
289             textColor = theme()->inactiveListBoxSelectionForegroundColor();
290  */
291           
292     }
293         
294     i.p->setPen(textColor);
295     
296     Font itemFont = style()->font();
297     if (element->hasTagName(optgroupTag)) {
298         FontDescription d = itemFont.fontDescription();
299         d.setBold(true);
300         itemFont = Font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
301         itemFont.update();
302     }
303     i.p->setFont(itemFont);
304     
305     // Draw the item text
306     if (itemStyle->visibility() != HIDDEN)
307         i.p->drawText(textRun, r.location());
308 }
309
310 void RenderListBox::paintItemBackground(PaintInfo& i, int tx, int ty, int listIndex)
311 {
312     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
313     const Vector<HTMLElement*>& listItems = select->listItems();
314     HTMLElement* element = listItems[listIndex];
315
316     Color backColor;
317     if (element->hasTagName(optionTag) && static_cast<HTMLOptionElement*>(element)->selected()) {
318         if (document()->frame()->isActive() && document()->focusNode() == node())
319             backColor = theme()->activeListBoxSelectionBackgroundColor();
320         else
321             backColor = theme()->inactiveListBoxSelectionBackgroundColor();
322     } else
323         backColor = element->renderStyle() ? element->renderStyle()->backgroundColor() : style()->backgroundColor();
324
325     // Draw the background for this list box item
326     if (!element->renderStyle() || element->renderStyle()->visibility() != HIDDEN)
327         i.p->fillRect(itemBoundingBoxRect(tx, ty, listIndex), backColor);
328 }
329
330 bool RenderListBox::isPointInScrollbar(NodeInfo& info, int _x, int _y, int _tx, int _ty)
331 {
332     if (!m_vBar)
333         return false;
334
335     IntRect vertRect(_tx + width() - borderRight() - m_vBar->width(),
336                    _ty + borderTop() - borderTopExtra(),
337                    m_vBar->width(),
338                    height() + borderTopExtra() + borderBottomExtra() - borderTop() - borderBottom());
339
340     if (vertRect.contains(_x, _y)) {
341         info.setScrollbar(m_vBar->isWidget() ? static_cast<PlatformScrollbar*>(m_vBar) : 0);
342         return true;
343     }
344     return false;
345 }
346
347 HTMLOptionElement* RenderListBox::optionAtPoint(int x, int y)
348 {
349     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
350     const Vector<HTMLElement*>& listItems = select->listItems();
351     int yOffset = y - absoluteBoundingBoxRect().y();
352     int newOffset = max(0, yOffset / (style()->font().height() + optionsSpacingMiddle)) + m_indexOffset;
353     newOffset = max(0, min((int)listItems.size() - 1, newOffset));
354     int scrollbarWidth = m_vBar ? m_vBar->width() : 0;
355     if (x >= absoluteBoundingBoxRect().x() + borderLeft() + paddingLeft() && x < absoluteBoundingBoxRect().right() - borderRight() - paddingRight() - scrollbarWidth)
356         return static_cast<HTMLOptionElement*>(listItems[newOffset]);
357     return 0;
358 }
359
360 void RenderListBox::autoscroll()
361 {
362     int mouseX = document()->frame()->view()->currentMousePosition().x();
363     int mouseY = document()->frame()->view()->currentMousePosition().y();
364     IntRect bounds = absoluteBoundingBoxRect();
365
366     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
367     const Vector<HTMLElement*>& items = select->listItems();
368     HTMLOptionElement* element = 0;
369     int rows = size();
370     int offset = m_indexOffset;
371     if (mouseY < bounds.y() && scrollToRevealElementAtListIndex(offset - 1) && items[offset - 1]->hasTagName(optionTag))
372         element = static_cast<HTMLOptionElement*>(items[offset - 1]);
373     else if (mouseY > bounds.bottom() && scrollToRevealElementAtListIndex(offset + rows) && items[offset + rows - 1]->hasTagName(optionTag))
374         element = static_cast<HTMLOptionElement*>(items[offset + rows - 1]);
375     else
376         element = optionAtPoint(mouseX, mouseY);
377         
378     if (element) {
379         select->setSelectedIndex(element->index(), !select->multiple());
380         repaint();
381     }
382 }
383
384 bool RenderListBox::scrollToRevealElementAtListIndex(int index)
385 {
386     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
387     const Vector<HTMLElement*>& listItems = select->listItems();
388     
389     if (index < 0 || index > (int)listItems.size() - 1 || (index >= m_indexOffset && index < m_indexOffset + size()))
390         return false;
391
392     int newOffset;
393     if (index < m_indexOffset)
394         newOffset = index;
395     else
396         newOffset = index - size() + 1;
397
398     if (m_vBar) {
399         IntRect rect = absoluteBoundingBoxRect();
400         m_vBar->setValue(itemBoundingBoxRect(rect.x(), rect.y(), newOffset + m_indexOffset).y() - rect.y());
401     }
402     m_indexOffset = newOffset;
403     
404     return true;
405 }
406
407 bool RenderListBox::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier)
408 {
409     return m_vBar && m_vBar->scroll(direction, granularity, multiplier);
410 }
411
412 void RenderListBox::valueChanged(unsigned listIndex)
413 {
414     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
415     select->setSelectedIndex(select->listToOptionIndex(listIndex));
416     select->onChange();
417 }
418
419 void RenderListBox::valueChanged(Scrollbar*)
420 {
421     if (m_vBar) {
422         int newOffset = max(0, m_vBar->value() / (style()->font().height() + optionsSpacingMiddle));
423         if (newOffset != m_indexOffset) {
424             m_indexOffset = newOffset;
425         //    printf("value changed: new offset index: %d\n", newOffset);
426             repaint();
427             // Fire the scroll DOM event.
428             EventTargetNodeCast(node())->dispatchHTMLEvent(scrollEvent, true, false);
429         }
430     }
431 }
432
433 }