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 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 (m_vBar && m_vBar->isWidget()) {
65         element()->document()->view()->removeChild(static_cast<PlatformScrollbar*>(m_vBar));
66         m_vBar->deref();
67     }
68 }
69
70
71 void RenderListBox::setStyle(RenderStyle* style)
72 {
73     RenderBlock::setStyle(style);
74     setReplaced(isInline());
75 }
76
77 void RenderListBox::updateFromElement()
78 {
79     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
80     const Vector<HTMLElement*>& listItems = select->listItems();
81     int size = listItems.size();
82     if (m_optionsChanged) {  
83         float width = 0;
84         TextStyle textStyle(0, 0, 0, false, false, false, false);
85         for (int i = 0; i < size; ++i) {
86             HTMLElement* element = listItems[i];
87             String text;
88             Font itemFont = style()->font();
89             if (element->hasTagName(optionTag))
90                 text = static_cast<HTMLOptionElement*>(element)->optionText();
91             else if (element->hasTagName(optgroupTag)) {
92                 text = static_cast<HTMLOptGroupElement*>(element)->groupLabelText();
93                 FontDescription d = itemFont.fontDescription();
94                 d.setBold(true);
95                 itemFont = Font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
96                 itemFont.update();
97             }
98                 
99             if (!text.isEmpty()) {
100                 float textWidth = itemFont.floatWidth(TextRun(text.impl()), textStyle);
101                 width = max(width, textWidth);
102             }
103         }
104         m_optionsWidth = static_cast<int>(ceilf(width));
105         m_optionsChanged = false;
106     }
107 }
108
109 void RenderListBox::calcMinMaxWidth()
110 {
111     if (!m_vBar) {
112         if (Scrollbar::hasPlatformScrollbars()) {
113             PlatformScrollbar* widget = new PlatformScrollbar(this, VerticalScrollbar, SmallScrollbar);
114             widget->ref();
115             node()->document()->view()->addChild(widget);
116             m_vBar = widget;
117         }
118     }
119     if (m_optionsChanged)
120         updateFromElement();
121
122     m_minWidth = 0;
123     m_maxWidth = 0;
124
125     if (style()->width().isFixed() && style()->width().value() > 0)
126         m_minWidth = m_maxWidth = calcContentBoxWidth(style()->width().value());
127     else {
128         m_maxWidth = m_optionsWidth;
129         if (m_vBar)
130            m_maxWidth += m_vBar->width();
131     }
132
133     if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) {
134         m_maxWidth = max(m_maxWidth, calcContentBoxWidth(style()->minWidth().value()));
135         m_minWidth = max(m_minWidth, calcContentBoxWidth(style()->minWidth().value()));
136     } else if (style()->width().isPercent() || (style()->width().isAuto() && style()->height().isPercent()))
137         m_minWidth = 0;
138     else
139         m_minWidth = m_maxWidth;
140
141     if (style()->maxWidth().isFixed() && style()->maxWidth().value() != undefinedLength) {
142         m_maxWidth = min(m_maxWidth, calcContentBoxWidth(style()->maxWidth().value()));
143         m_minWidth = min(m_minWidth, calcContentBoxWidth(style()->maxWidth().value()));
144     }
145
146     int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight();
147     m_minWidth += toAdd;
148     m_maxWidth += toAdd;
149                                 
150     setMinMaxKnown();
151 }
152
153 const int minSize = 4;
154 const int minDefaultSize = 10;
155 int RenderListBox::size() const
156 {
157     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
158     int specifiedSize = select->size();
159     if (specifiedSize > 1)
160         return max(minSize, specifiedSize);
161
162     return min(max(numItems(), minSize), minDefaultSize);
163 }
164
165 int RenderListBox::numItems() const
166 {
167     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
168     return select->listItems().size();
169 }
170
171 void RenderListBox::calcHeight()
172 {
173     int toAdd = paddingTop() + paddingBottom() + borderTop() + borderBottom();
174
175     int itemHeight = style()->font().height() + optionsSpacingMiddle;
176     m_height = itemHeight * size() + toAdd;
177     
178     RenderBlock::calcHeight();
179     
180     if (m_vBar) {
181         m_vBar->setEnabled(size() < numItems());
182         m_vBar->setSteps(itemHeight, itemHeight);
183         m_vBar->setProportion(m_height - toAdd, itemHeight * numItems());
184     }
185 }
186
187 const int baselineAdjustment = 7;
188 short RenderListBox::baselinePosition(bool b, bool isRootLineBox) const
189 {
190     // 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.
191     return height() + marginTop() + marginBottom() - baselineAdjustment;
192 }
193
194 IntRect RenderListBox::itemBoundingBoxRect(int tx, int ty, int index)
195 {
196     return IntRect (tx + borderLeft() + paddingLeft(),
197                    ty + borderTop() + paddingTop() + ((style()->font().height() + optionsSpacingMiddle) * (index - m_indexOffset)),
198                    absoluteBoundingBoxRect().width() - borderLeft() - borderRight() - paddingLeft() - paddingRight(),
199                    style()->font().height() + optionsSpacingMiddle);
200 }
201     
202 void RenderListBox::paintObject(PaintInfo& i, int tx, int ty)
203 {
204     // Push a clip.
205     IntRect clipRect(tx + borderLeft(), ty + borderTop(),
206          width() - borderLeft() - borderRight(), height() - borderBottom() - borderTop());
207     if (i.phase == PaintPhaseForeground || i.phase == PaintPhaseChildBlockBackgrounds) {
208         if (clipRect.width() == 0 || clipRect.height() == 0)
209             return;
210         i.p->save();
211         i.p->clip(clipRect);
212     }
213     
214     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
215     int listItemsSize = select->listItems().size();
216
217     if (i.phase == PaintPhaseForeground) {
218         int index = m_indexOffset;
219         while (index < listItemsSize && index < m_indexOffset + size()) {
220             paintItemForeground(i, tx, ty, index);
221             index++;
222         }
223     }
224     
225     // Paint the children.
226     RenderBlock::paintObject(i, tx, ty);
227     
228     if (i.phase == PaintPhaseBlockBackground) {
229         int index = m_indexOffset;
230         while (index < listItemsSize && index < m_indexOffset + size()) {
231             paintItemBackground(i, tx, ty, index);
232             index++;
233         }
234         paintScrollbar(i);
235     }
236     // Pop the clip.
237     if (i.phase == PaintPhaseForeground || i.phase == PaintPhaseChildBlockBackgrounds)
238         i.p->restore();
239 }
240
241 void RenderListBox::paintScrollbar(PaintInfo& i)
242 {
243     if (m_vBar) {
244         IntRect absBounds = absoluteBoundingBoxRect();
245         IntRect scrollRect(absBounds.right() - borderRight() - m_vBar->width(),
246                 absBounds.y() + borderTop(),
247                 m_vBar->width(),
248                 absBounds.height() - (borderTop() + borderBottom()));
249         m_vBar->setRect(scrollRect);
250         m_vBar->paint(i.p, scrollRect);
251     }
252 }
253
254 void RenderListBox::paintItemForeground(PaintInfo& i, int tx, int ty, int listIndex)
255 {
256     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
257     const Vector<HTMLElement*>& listItems = select->listItems();
258     HTMLElement* element = listItems[listIndex];
259             
260     String itemText;
261     if (element->hasTagName(optionTag))
262         itemText = static_cast<HTMLOptionElement*>(element)->optionText();
263     else if (element->hasTagName(optgroupTag))
264         itemText = static_cast<HTMLOptGroupElement*>(element)->groupLabelText();
265    
266     TextRun textRun(itemText.characters(), itemText.length());
267     
268     // Determine where the item text should be placed
269     IntRect r = itemBoundingBoxRect(tx, ty, listIndex);
270     r.move(optionsSpacingLeft, style()->font().ascent());
271
272     RenderStyle* itemStyle = element->renderStyle();
273     if (!itemStyle)
274         itemStyle = style();
275     
276     Color textColor = element->renderStyle() ? element->renderStyle()->color() : style()->color();
277     if (element->hasTagName(optionTag) && static_cast<HTMLOptionElement*>(element)->selected()) {
278         if (document()->frame()->isActive() && document()->focusNode() == node())
279             textColor = theme()->activeListBoxSelectionForegroundColor();
280  /*
281     FIXME: Decide what the desired behavior is for inactive foreground color.  
282     For example, disabled items have a dark grey foreground color defined in CSS.  
283     If we don't honor that, the selected disabled items will have a weird black-on-grey look.
284         else
285             textColor = theme()->inactiveListBoxSelectionForegroundColor();
286  */
287           
288     }
289         
290     i.p->setPen(textColor);
291     
292     Font itemFont = style()->font();
293     if (element->hasTagName(optgroupTag)) {
294         FontDescription d = itemFont.fontDescription();
295         d.setBold(true);
296         itemFont = Font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
297         itemFont.update();
298     }
299     i.p->setFont(itemFont);
300     
301     // Draw the item text
302     if (itemStyle->visibility() != HIDDEN)
303         i.p->drawText(textRun, r.location());
304 }
305
306 void RenderListBox::paintItemBackground(PaintInfo& i, int tx, int ty, int listIndex)
307 {
308     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
309     const Vector<HTMLElement*>& listItems = select->listItems();
310     HTMLElement* element = listItems[listIndex];
311
312     Color backColor;
313     if (element->hasTagName(optionTag) && static_cast<HTMLOptionElement*>(element)->selected()) {
314         if (document()->frame()->isActive() && document()->focusNode() == node())
315             backColor = theme()->activeListBoxSelectionBackgroundColor();
316         else
317             backColor = theme()->inactiveListBoxSelectionBackgroundColor();
318     } else
319         backColor = element->renderStyle() ? element->renderStyle()->backgroundColor() : style()->backgroundColor();
320
321     // Draw the background for this list box item
322     if (!element->renderStyle() || element->renderStyle()->visibility() != HIDDEN)
323         i.p->fillRect(itemBoundingBoxRect(tx, ty, listIndex), backColor);
324 }
325
326 bool RenderListBox::isPointInScrollbar(NodeInfo& info, int _x, int _y, int _tx, int _ty)
327 {
328     if (!m_vBar)
329         return false;
330
331     IntRect vertRect(_tx + width() - borderRight() - m_vBar->width(),
332                    _ty + borderTop() - borderTopExtra(),
333                    m_vBar->width(),
334                    height() + borderTopExtra() + borderBottomExtra() - borderTop() - borderBottom());
335
336     if (vertRect.contains(_x, _y)) {
337         info.setScrollbar(m_vBar->isWidget() ? static_cast<PlatformScrollbar*>(m_vBar) : 0);
338         return true;
339     }
340     return false;
341 }
342
343 HTMLOptionElement* RenderListBox::optionAtPoint(int x, int y)
344 {
345     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
346     const Vector<HTMLElement*>& listItems = select->listItems();
347     int yOffset = y - absoluteBoundingBoxRect().y();
348     int newOffset = max(0, yOffset / (style()->font().height() + optionsSpacingMiddle)) + m_indexOffset;
349     newOffset = max(0, min((int)listItems.size() - 1, newOffset));
350     int scrollbarWidth = m_vBar ? m_vBar->width() : 0;
351     if (x >= absoluteBoundingBoxRect().x() + borderLeft() + paddingLeft() && x < absoluteBoundingBoxRect().right() - borderRight() - paddingRight() - scrollbarWidth)
352         return static_cast<HTMLOptionElement*>(listItems[newOffset]);
353     return 0;
354 }
355
356 void RenderListBox::autoscroll()
357 {
358     IntPoint pos = document()->frame()->view()->windowToContents(document()->frame()->view()->currentMousePosition());
359     IntRect bounds = absoluteBoundingBoxRect();
360
361     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
362     const Vector<HTMLElement*>& items = select->listItems();
363     HTMLOptionElement* element = 0;
364     int rows = size();
365     int offset = m_indexOffset;
366     if (pos.y() < bounds.y() && scrollToRevealElementAtListIndex(offset - 1) && items[offset - 1]->hasTagName(optionTag))
367         element = static_cast<HTMLOptionElement*>(items[offset - 1]);
368     else if (pos.y() > bounds.bottom() && scrollToRevealElementAtListIndex(offset + rows) && items[offset + rows - 1]->hasTagName(optionTag))
369         element = static_cast<HTMLOptionElement*>(items[offset + rows - 1]);
370     else
371         element = optionAtPoint(pos.x(), pos.y());
372         
373     if (element) {
374         select->setSelectedIndex(element->index(), !select->multiple());
375         repaint();
376     }
377 }
378
379 bool RenderListBox::scrollToRevealElementAtListIndex(int index)
380 {
381     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
382     const Vector<HTMLElement*>& listItems = select->listItems();
383     
384     if (index < 0 || index > (int)listItems.size() - 1 || (index >= m_indexOffset && index < m_indexOffset + size()))
385         return false;
386
387     int newOffset;
388     if (index < m_indexOffset)
389         newOffset = index;
390     else
391         newOffset = index - size() + 1;
392
393     if (m_vBar) {
394         IntRect rect = absoluteBoundingBoxRect();
395         m_vBar->setValue(itemBoundingBoxRect(rect.x(), rect.y(), newOffset + m_indexOffset).y() - rect.y());
396     }
397     m_indexOffset = newOffset;
398     
399     return true;
400 }
401
402 bool RenderListBox::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier)
403 {
404     return m_vBar && m_vBar->scroll(direction, granularity, multiplier);
405 }
406
407 void RenderListBox::valueChanged(unsigned listIndex)
408 {
409     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
410     select->setSelectedIndex(select->listToOptionIndex(listIndex));
411     select->onChange();
412 }
413
414 void RenderListBox::valueChanged(Scrollbar*)
415 {
416     if (m_vBar) {
417         int newOffset = max(0, m_vBar->value() / (style()->font().height() + optionsSpacingMiddle));
418         if (newOffset != m_indexOffset) {
419             m_indexOffset = newOffset;
420         //    printf("value changed: new offset index: %d\n", newOffset);
421             repaint();
422             // Fire the scroll DOM event.
423             EventTargetNodeCast(node())->dispatchHTMLEvent(scrollEvent, true, false);
424         }
425     }
426 }
427
428 }