Reviewed by Hyatt.
[WebKit-https.git] / WebCore / rendering / render_list.cpp
1 /**
2  * This file is part of the DOM implementation for KDE.
3  *
4  * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
5  *           (C) 1999 Antti Koivisto (koivisto@kde.org)
6  * Copyright (C) 2003 Apple Computer, Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Library General Public
10  * License as published by the Free Software Foundation; either
11  * version 2 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * Library General Public License for more details.
17  *
18  * You should have received a copy of the GNU Library General Public License
19  * along with this library; see the file COPYING.LIB.  If not, write to
20  * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
21  * Boston, MA 02111-1307, USA.
22  *
23  */
24
25 #include "config.h"
26 #include "render_list.h"
27 #include "rendering/render_canvas.h"
28
29 #include "DocumentImpl.h"
30 #include "CachedImage.h"
31
32 #include "htmlnames.h"
33
34 #include <qpainter.h>
35 #include "Pen.h"
36
37 #include "helper.h"
38
39 #include <kdebug.h>
40
41 //#define BOX_DEBUG
42
43 using namespace DOM::HTMLNames;
44 using namespace khtml;
45
46 using DOM::DocumentImpl;
47
48 const int cMarkerPadding = 7;
49
50 static QString toRoman( int number, bool upper )
51 {
52     QString roman;
53     QChar ldigits[] = { 'i', 'v', 'x', 'l', 'c', 'd', 'm' };
54     QChar udigits[] = { 'I', 'V', 'X', 'L', 'C', 'D', 'M' };
55     QChar *digits = upper ? udigits : ldigits;
56     int i, d = 0;
57
58     do
59     {
60         int num = number % 10;
61
62         if ( num % 5 < 4 )
63             for ( i = num % 5; i > 0; i-- )
64                 roman.insert( 0, digits[ d ] );
65
66         if ( num >= 4 && num <= 8)
67             roman.insert( 0, digits[ d+1 ] );
68
69         if ( num == 9 )
70             roman.insert( 0, digits[ d+2 ] );
71
72         if ( num % 5 == 4 )
73             roman.insert( 0, digits[ d ] );
74
75         number /= 10;
76         d += 2;
77     }
78     while ( number );
79
80     return roman;
81 }
82
83 static QString toLetter( int number, int base ) {
84     number--;
85     QString letter = (QChar) (base + (number % 26));
86     // Add a single quote at the end of the alphabet.
87     for (int i = 0; i < (number / 26); i++) {
88        letter += '\'';
89     }
90     return letter;
91 }
92
93 static QString toHebrew( int number ) {
94     const QChar tenDigit[] = {1497, 1499, 1500, 1502, 1504, 1505, 1506, 1508, 1510};
95
96     QString letter;
97     if (number>999) {
98         letter = toHebrew(number/1000) + QString::fromLatin1("'");
99         number = number%1000;
100     }
101
102     int hunderts = (number/400);
103     if (hunderts > 0) {
104         for(int i=0; i<hunderts; i++) {
105             letter += QChar(1511 + 3);
106         }
107     }
108     number = number % 400;
109     if ((number / 100) != 0) {
110         letter += QChar (1511 + (number / 100) -1);
111     }
112     number = number % 100;
113     int tens = number/10;
114     if (tens > 0 && !(number == 15 || number == 16)) {
115         letter += tenDigit[tens-1];
116     }
117     if (number == 15 || number == 16) { // special because of religious
118         letter += QChar(1487 + 9);       // reasons
119         letter += QChar(1487 + number - 9);
120     } else {
121         number = number % 10;
122         if (number != 0) {
123             letter += QChar (1487 + number);
124         }
125     }
126     return letter;
127 }
128
129 // -------------------------------------------------------------------------
130
131 RenderListItem::RenderListItem(DOM::NodeImpl* node)
132     : RenderBlock(node), _notInList(false)
133 {
134     // init RenderObject attributes
135     setInline(false);   // our object is not Inline
136
137     predefVal = -1;
138     m_marker = 0;
139 }
140
141 void RenderListItem::setStyle(RenderStyle *_style)
142 {
143     RenderBlock::setStyle(_style);
144
145     if (style()->listStyleType() != LNONE ||
146         (style()->listStyleImage() && !style()->listStyleImage()->isErrorImage())) {
147         RenderStyle *newStyle = new (renderArena()) RenderStyle();
148         newStyle->ref();
149         // The marker always inherits from the list item, regardless of where it might end
150         // up (e.g., in some deeply nested line box).  See CSS3 spec.
151         newStyle->inheritFrom(style()); 
152         if (!m_marker) {
153             m_marker = new (renderArena()) RenderListMarker(document());
154             m_marker->setStyle(newStyle);
155             m_marker->setListItem(this);
156         } else
157             m_marker->setStyle(newStyle);
158         newStyle->deref(renderArena());
159     } else if (m_marker) {
160         m_marker->destroy();
161         m_marker = 0;
162     }
163 }
164
165 RenderListItem::~RenderListItem()
166 {
167 }
168
169 void RenderListItem::destroy()
170 {    
171     if (m_marker) {
172         m_marker->destroy();
173         m_marker = 0;
174     }
175     RenderBlock::destroy();
176 }
177
178 void RenderListItem::calcListValue()
179 {
180     // only called from the marker so..
181     KHTMLAssert(m_marker);
182
183     if (predefVal != -1)
184         m_marker->m_value = predefVal;
185     else if (!previousSibling())
186         m_marker->m_value = 1;
187     else {
188         RenderObject *o = previousSibling();
189         while (o && (!o->isListItem() || o->style()->listStyleType() == LNONE))
190             o = o->previousSibling();
191         if (o && o->isListItem() && o->style()->listStyleType() != LNONE) {
192             RenderListItem *item = static_cast<RenderListItem *>(o);
193             m_marker->m_value = item->value() + 1;
194         } else
195             m_marker->m_value = 1;
196     }
197 }
198
199 bool RenderListItem::isEmpty() const
200 {
201     return lastChild() == m_marker;
202 }
203
204 static RenderObject* getParentOfFirstLineBox(RenderObject* curr, RenderObject* marker)
205 {
206     RenderObject* firstChild = curr->firstChild();
207     if (!firstChild)
208         return 0;
209         
210     for (RenderObject* currChild = firstChild; currChild; currChild = currChild->nextSibling()) {
211         if (currChild == marker)
212             continue;
213             
214         if (currChild->isInline())
215             return curr;
216         
217         if (currChild->isFloating() || currChild->isPositioned())
218             continue;
219             
220         if (currChild->isTable() || !currChild->isRenderBlock())
221             break;
222         
223         if (currChild->style()->htmlHacks() && currChild->element() &&
224             (currChild->element()->hasTagName(ulTag)|| currChild->element()->hasTagName(olTag)))
225             break;
226             
227         RenderObject* lineBox = getParentOfFirstLineBox(currChild, marker);
228         if (lineBox)
229             return lineBox;
230     }
231     
232     return 0;
233 }
234
235 void RenderListItem::resetMarkerValue()
236 {
237     if (!m_marker)
238         return;
239
240     m_marker->m_value = -1;
241     m_marker->setNeedsLayoutAndMinMaxRecalc();
242 }
243
244 void RenderListItem::updateMarkerLocation()
245 {
246     // Sanity check the location of our marker.
247     if (m_marker) {
248         RenderObject* markerPar = m_marker->parent();
249         RenderObject* lineBoxParent = getParentOfFirstLineBox(this, m_marker);
250         if (!lineBoxParent) {
251             // If the marker is currently contained inside an anonymous box,
252             // then we are the only item in that anonymous box (since no line box
253             // parent was found).  It's ok to just leave the marker where it is
254             // in this case.
255             if (markerPar && markerPar->isAnonymousBlock())
256                 lineBoxParent = markerPar;
257             else
258                 lineBoxParent = this;
259         }
260         
261         if (markerPar != lineBoxParent || !m_marker->minMaxKnown()) {
262             if (markerPar)
263                 markerPar->removeChild(m_marker);
264             if (!lineBoxParent)
265                 lineBoxParent = this;
266             lineBoxParent->addChild(m_marker, lineBoxParent->firstChild());
267             if (!m_marker->minMaxKnown())
268                 m_marker->calcMinMaxWidth();
269             recalcMinMaxWidths();
270         }
271     }
272 }
273
274 void RenderListItem::calcMinMaxWidth()
275 {
276     // Make sure our marker is in the correct location.
277     updateMarkerLocation();
278     if (!minMaxKnown())
279         RenderBlock::calcMinMaxWidth();
280 }
281
282 void RenderListItem::layout( )
283 {
284     KHTMLAssert( needsLayout() );
285     KHTMLAssert( minMaxKnown() );
286
287     updateMarkerLocation();    
288     RenderBlock::layout();
289 }
290
291 void RenderListItem::paint(PaintInfo& i, int _tx, int _ty)
292 {
293     if (!m_height)
294         return;
295
296     RenderBlock::paint(i, _tx, _ty);
297 }
298
299 IntRect RenderListItem::getAbsoluteRepaintRect()
300 {
301     IntRect result = RenderBlock::getAbsoluteRepaintRect();
302     if (m_marker && !m_marker->isInside()) {
303         // This can be a sloppy and imprecise offset as long as it's always too big.
304         int pixHeight = style()->htmlFont().getFontDef().computedPixelSize();
305         int offset = pixHeight*2/3;
306         bool haveImage = m_marker->listImage() && !m_marker->listImage()->isErrorImage();
307         if (haveImage)
308             offset = m_marker->listImage()->image().width();
309         int bulletWidth = offset/2;
310         if (offset%2)
311             bulletWidth++;
312         int xoff = 0;
313         if (style()->direction() == LTR)
314             xoff = -cMarkerPadding - offset;
315         else
316             xoff = cMarkerPadding + (haveImage ? 0 : (offset - bulletWidth));
317
318         if (xoff < 0) {
319             result.setX(result.x() + xoff);
320             result.setWidth(result.width() - xoff);
321         } else
322             result.setWidth(result.width() + xoff);
323     }
324     return result;
325 }
326
327 // -----------------------------------------------------------
328
329 RenderListMarker::RenderListMarker(DocumentImpl* document)
330     : RenderBox(document), m_listImage(0), m_value(-1)
331 {
332     // init RenderObject attributes
333     setInline(true);   // our object is Inline
334     setReplaced(true); // pretend to be replaced
335     // val = -1;
336     // m_listImage = 0;
337 }
338
339 RenderListMarker::~RenderListMarker()
340 {
341     if (m_listImage)
342         m_listImage->deref(this);
343 }
344
345 void RenderListMarker::setStyle(RenderStyle *s)
346 {
347     if (s && style() && s->listStylePosition() != style()->listStylePosition())
348         setNeedsLayoutAndMinMaxRecalc();
349     
350     RenderBox::setStyle(s);
351
352     if ( m_listImage != style()->listStyleImage() ) {
353         if (m_listImage)
354             m_listImage->deref(this);
355         m_listImage = style()->listStyleImage();
356         if (m_listImage)
357             m_listImage->ref(this);
358     }
359 }
360
361 InlineBox* RenderListMarker::createInlineBox(bool, bool isRootLineBox, bool)
362 {
363     KHTMLAssert(!isRootLineBox);
364     ListMarkerBox* box = new (renderArena()) ListMarkerBox(this);
365     m_inlineBoxWrapper = box;
366     return box;
367 }
368
369 void RenderListMarker::paint(PaintInfo& i, int _tx, int _ty)
370 {
371     if (i.phase != PaintActionForeground)
372         return;
373     
374     if (style()->visibility() != VISIBLE)  return;
375
376     _tx += m_x;
377     _ty += m_y;
378
379     if ((_ty > i.r.y() + i.r.height()) || (_ty + m_height < i.r.y()))
380         return;
381
382     if (shouldPaintBackgroundOrBorder()) 
383         paintBoxDecorations(i, _tx, _ty);
384
385     QPainter* p = i.p;
386     p->setFont(style()->font());
387     const QFontMetrics fm = p->fontMetrics();
388     
389     // The marker needs to adjust its tx, for the case where it's an outside marker.
390     RenderObject* listItem = 0;
391     int leftLineOffset = 0;
392     int rightLineOffset = 0;
393     if (!isInside()) {
394         listItem = this;
395         int yOffset = 0;
396         int xOffset = 0;
397         while (listItem && listItem != m_listItem) {
398             yOffset += listItem->yPos();
399             xOffset += listItem->xPos();
400             listItem = listItem->parent();
401         }
402         
403         // Now that we have our xoffset within the listbox, we need to adjust ourselves by the delta
404         // between our current xoffset and our desired position (which is just outside the border box
405         // of the list item).
406         if (style()->direction() == LTR) {
407             leftLineOffset = m_listItem->leftRelOffset(yOffset, m_listItem->leftOffset(yOffset));
408             _tx -= (xOffset - leftLineOffset) + m_listItem->paddingLeft() + m_listItem->borderLeft();
409         } else {
410             rightLineOffset = m_listItem->rightRelOffset(yOffset, m_listItem->rightOffset(yOffset));
411             _tx += (rightLineOffset-xOffset) + m_listItem->paddingRight() + m_listItem->borderRight();
412         }
413     }
414
415     if (p->printing()) {
416         if (_ty < i.r.y())
417             // This has been printed already we suppose.
418             return;
419         
420         RenderCanvas* c = canvas();
421         if (_ty + m_height + paddingBottom() + borderBottom() >= c->printRect().y() + c->printRect().height()) {
422             if (_ty < c->truncatedAt())
423                 c->setBestTruncatedAt(_ty, this);
424             // Let's print this on the next page.
425             return; 
426         }
427     }
428     
429     int offset = fm.ascent()*2/3;
430     bool haveImage = m_listImage && !m_listImage->isErrorImage();
431     if (haveImage)
432         offset = m_listImage->image().width();
433     
434     int xoff = 0;
435     int yoff = fm.ascent() - offset;
436
437     int bulletWidth = offset/2;
438     if (offset%2)
439         bulletWidth++;
440     if (!isInside()) {
441         if (listItem->style()->direction() == LTR)
442             xoff = -cMarkerPadding - offset;
443         else
444             xoff = cMarkerPadding + (haveImage ? 0 : (offset - bulletWidth));
445     } else if (style()->direction() == RTL)
446         xoff += haveImage ? cMarkerPadding : (m_width - bulletWidth);
447     
448     if (m_listImage && !m_listImage->isErrorImage()) {
449         p->drawImageAtPoint(m_listImage->image(), IntPoint(_tx + xoff, _ty));
450         return;
451     }
452
453 #ifdef BOX_DEBUG
454     p->setPen( Qt::red );
455     p->drawRect( _tx + xoff, _ty + yoff, offset, offset );
456 #endif
457
458     const Color color( style()->color() );
459     p->setPen( color );
460
461     switch(style()->listStyleType()) {
462     case DISC:
463         p->setBrush(color);
464         p->drawEllipse(_tx + xoff, _ty + (3 * yoff)/2, bulletWidth, bulletWidth);
465         return;
466     case CIRCLE:
467         p->setBrush(WebCore::Brush::NoBrush);
468         p->drawEllipse(_tx + xoff, _ty + (3 * yoff)/2, bulletWidth, bulletWidth);
469         return;
470     case SQUARE:
471         p->setBrush(color);
472         p->drawRect(_tx + xoff, _ty + (3 * yoff)/2, bulletWidth, bulletWidth);
473         return;
474     case LNONE:
475         return;
476     default:
477         if (!m_item.isEmpty()) {
478             // Text should be drawn on the baseline, so we add in the ascent of the font. 
479             // For some inexplicable reason, this works in Konqueror.  I'm not sure why.
480             // - dwh
481             _ty += fm.ascent();
482
483             if (isInside()) {
484                 if( style()->direction() == LTR) {
485                     p->drawText(_tx, _ty, 0, 0, 0, 0, Qt::AlignLeft|Qt::DontClip, m_item);
486                     p->drawText(_tx + fm.width(m_item, 0, 0), _ty, 0, 0, 0, 0, Qt::AlignLeft|Qt::DontClip, 
487                             QString::fromLatin1(". "));
488                 } else {
489                     const QString& punct(QString::fromLatin1(" ."));
490                     p->drawText(_tx, _ty, 0, 0, 0, 0, Qt::AlignLeft|Qt::DontClip, punct);
491                     p->drawText(_tx + fm.width(punct, 0, 0), _ty, 0, 0, 0, 0, Qt::AlignLeft|Qt::DontClip, m_item);
492                 }
493             } else {
494                 if (style()->direction() == LTR) {
495                     const QString& punct(QString::fromLatin1(". "));
496                     p->drawText(_tx-offset/2, _ty, 0, 0, 0, 0, Qt::AlignRight|Qt::DontClip, punct);
497                     p->drawText(_tx-offset/2-fm.width(punct, 0, 0), _ty, 0, 0, 0, 0, Qt::AlignRight|Qt::DontClip, m_item);
498                 } else {
499                     const QString& punct(QString::fromLatin1(" ."));
500                     p->drawText(_tx+offset/2, _ty, 0, 0, 0, 0, Qt::AlignLeft|Qt::DontClip, punct);
501                     p->drawText(_tx+offset/2+fm.width(punct, 0, 0), _ty, 0, 0, 0, 0, Qt::AlignLeft|Qt::DontClip, m_item);
502                 }
503             }
504         }
505     }
506 }
507
508 void RenderListMarker::layout()
509 {
510     KHTMLAssert( needsLayout() );
511     // ### KHTMLAssert( minMaxKnown() );
512     if ( !minMaxKnown() )
513         calcMinMaxWidth();
514     setNeedsLayout(false);
515 }
516
517 void RenderListMarker::imageChanged(CachedImage *o, const IntRect& r)
518 {
519     if (o != m_listImage) {
520         RenderBox::imageChanged(o, r);
521         return;
522     }
523
524     if (m_width != m_listImage->imageSize().width() || m_height != m_listImage->imageSize().height())
525         setNeedsLayoutAndMinMaxRecalc();
526     else
527         repaint();
528 }
529
530 void RenderListMarker::calcMinMaxWidth()
531 {
532     KHTMLAssert( !minMaxKnown() );
533
534     m_width = 0;
535
536     if (m_listImage) {
537         if (isInside())
538             m_width = m_listImage->image().width() + cMarkerPadding;
539         m_height = m_listImage->image().height();
540         m_minWidth = m_maxWidth = m_width;
541         setMinMaxKnown();
542         return;
543     }
544
545     if (m_value < 0) // not yet calculated
546         m_listItem->calcListValue();
547
548     const QFontMetrics &fm = style()->fontMetrics();
549     m_height = fm.ascent();
550
551     switch(style()->listStyleType())
552     {
553     case DISC:
554     case CIRCLE:
555     case SQUARE:
556         if (isInside())
557             m_width = m_height;
558         goto end;
559     case ARMENIAN:
560     case GEORGIAN:
561     case CJK_IDEOGRAPHIC:
562     case HIRAGANA:
563     case KATAKANA:
564     case HIRAGANA_IROHA:
565     case KATAKANA_IROHA:
566     case DECIMAL_LEADING_ZERO:
567         // ### unsupported, we use decimal instead
568     case LDECIMAL:
569         m_item.sprintf( "%d", m_value );
570         break;
571     case LOWER_ROMAN:
572         m_item = toRoman( m_value, false );
573         break;
574     case UPPER_ROMAN:
575         m_item = toRoman( m_value, true );
576         break;
577     case LOWER_GREEK:
578      {
579         int number = m_value - 1;
580         int l = (number % 24);
581
582         if (l>16) {l++;} // Skip GREEK SMALL LETTER FINAL SIGMA
583
584         m_item = QChar(945 + l);
585         for (int i = 0; i < (number / 24); i++) {
586             m_item += QString::fromLatin1("'");
587         }
588         break;
589      }
590     case HEBREW:
591         m_item = toHebrew( m_value );
592         break;
593     case LOWER_ALPHA:
594     case LOWER_LATIN:
595         m_item = toLetter( m_value, 'a' );
596         break;
597     case UPPER_ALPHA:
598     case UPPER_LATIN:
599         m_item = toLetter( m_value, 'A' );
600         break;
601     case LNONE:
602         break;
603     }
604
605     if (isInside()) {
606         m_width = fm.width(m_item, 0, 0) + fm.width(QString::fromLatin1(". "), 0, 0);
607     }
608
609 end:
610
611     m_minWidth = m_width;
612     m_maxWidth = m_width;
613
614     setMinMaxKnown();
615 }
616
617 void RenderListMarker::calcWidth()
618 {
619     RenderBox::calcWidth();
620 }
621
622 short RenderListMarker::lineHeight(bool, bool) const
623 {
624     if (!m_listImage)
625         return m_listItem->lineHeight(false, true);
626     return height();
627 }
628
629 short RenderListMarker::baselinePosition(bool, bool) const
630 {
631     if (!m_listImage) {
632         const QFontMetrics &fm = style()->fontMetrics();
633         return fm.ascent() + (lineHeight(false) - fm.height())/2;
634     }
635     return height();
636 }
637
638 bool RenderListMarker::isInside() const
639 {
640     return m_listItem->notInList() || style()->listStylePosition() == INSIDE;
641 }
642
643 #undef BOX_DEBUG