You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdelibs/khtml/rendering/render_text.cpp

1547 lines
47 KiB

/**
* This file is part of the DOM implementation for KDE.
*
* Copyright (C) 1999-2003 Lars Knoll (knoll@kde.org)
* (C) 2000-2003 Dirk Mueller (mueller@kde.org)
* (C) 2003 Apple Computer, Inc.
* (C) 2004-2005 Allan Sandfeld Jensen (kde@carewolf.com)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
//#define DEBUG_LAYOUT
//#define BIDI_DEBUG
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "rendering/render_text.h"
#include "rendering/render_canvas.h"
#include "rendering/break_lines.h"
#include "rendering/render_arena.h"
#include "xml/dom_nodeimpl.h"
#include "misc/loader.h"
#include "misc/helper.h"
#include <qbitmap.h>
#include <qimage.h>
#include <qpainter.h>
#include <kdebug.h>
#include <kglobal.h>
#include <assert.h>
#include <limits.h>
#include <math.h>
#ifdef HAVE_ALLOCA_H
// explicitly included for systems that don't provide it in stdlib.h
#include <alloca.h>
#else
#include <stdlib.h>
#endif
using namespace khtml;
using namespace DOM;
#ifndef NDEBUG
static bool inInlineTextBoxDetach;
#endif
void InlineTextBox::detach(RenderArena* renderArena)
{
if (m_parent)
m_parent->removeFromLine(this);
#ifndef NDEBUG
inInlineTextBoxDetach = true;
#endif
delete this;
#ifndef NDEBUG
inInlineTextBoxDetach = false;
#endif
// Recover the size left there for us by operator delete and free the memory.
renderArena->free(*(size_t *)this, this);
}
void* InlineTextBox::operator new(size_t sz, RenderArena* renderArena) throw()
{
return renderArena->allocate(sz);
}
void InlineTextBox::operator delete(void* ptr, size_t sz)
{
assert(inInlineTextBoxDetach);
// Stash size where detach can find it.
*(size_t *)ptr = sz;
}
void InlineTextBox::selectionStartEnd(int& sPos, int& ePos)
{
int startPos, endPos;
if (object()->selectionState() == RenderObject::SelectionInside) {
startPos = 0;
endPos = renderText()->string()->l;
} else {
renderText()->selectionStartEnd(startPos, endPos);
if (object()->selectionState() == RenderObject::SelectionStart)
endPos = renderText()->string()->l;
else if (object()->selectionState() == RenderObject::SelectionEnd)
startPos = 0;
}
sPos = kMax(startPos - m_start, 0);
ePos = kMin(endPos - m_start, (int)m_len);
}
RenderObject::SelectionState InlineTextBox::selectionState()
{
RenderObject::SelectionState state = object()->selectionState();
if (state == RenderObject::SelectionStart || state == RenderObject::SelectionEnd ||
state == RenderObject::SelectionBoth) {
int startPos, endPos;
renderText()->selectionStartEnd(startPos, endPos);
bool start = (state != RenderObject::SelectionEnd && startPos >= m_start && startPos < m_start + m_len);
bool end = (state != RenderObject::SelectionStart && endPos > m_start && endPos <= m_start + m_len);
if (start && end)
state = RenderObject::SelectionBoth;
else if (start)
state = RenderObject::SelectionStart;
else if (end)
state = RenderObject::SelectionEnd;
else if ((state == RenderObject::SelectionEnd || startPos < m_start) &&
(state == RenderObject::SelectionStart || endPos > m_start + m_len))
state = RenderObject::SelectionInside;
}
return state;
}
void InlineTextBox::paint(RenderObject::PaintInfo& i, int tx, int ty)
{
if (object()->isBR() || object()->style()->visibility() != VISIBLE ||
m_truncation == cFullTruncation || i.phase == PaintActionOutline)
return;
if (i.phase == PaintActionSelection && object()->selectionState() == RenderObject::SelectionNone)
// When only painting the selection, don't bother to paint if there is none.
return;
int xPos = tx + m_x;
int w = width();
if ((xPos >= i.r.x() + i.r.width()) || (xPos + w <= i.r.x()))
return;
// Set our font.
RenderStyle* styleToUse = object()->style(m_firstLine);
int d = styleToUse->textDecorationsInEffect();
if (styleToUse->font() != i.p->font())
i.p->setFont(styleToUse->font());
const Font *font = &styleToUse->htmlFont();
bool haveSelection = selectionState() != RenderObject::SelectionNone;
// Now calculate startPos and endPos, for painting selection.
// We paint selection while endPos > 0
int ePos = 0, sPos = 0;
if (haveSelection && !object()->canvas()->staticMode()) {
selectionStartEnd(sPos, ePos);
}
if (styleToUse->color() != i.p->pen().color())
i.p->setPen(styleToUse->color());
if (m_len > 0 && i.phase != PaintActionSelection) {
int endPoint = m_len;
if (m_truncation != cNoTruncation)
endPoint = m_truncation - m_start;
if (styleToUse->textShadow())
paintShadow(i.p, font, tx, ty, styleToUse->textShadow());
if (!haveSelection || sPos != 0 || ePos != m_len) {
font->drawText(i.p, m_x + tx, m_y + ty + m_baseline, renderText()->string()->s, renderText()->string()->l, m_start, endPoint,
m_toAdd, m_reversed ? QPainter::RTL : QPainter::LTR);
}
}
if (d != TDNONE && i.phase != PaintActionSelection && styleToUse->htmlHacks()) {
i.p->setPen(styleToUse->color());
paintDecoration(i.p, font, tx, ty, d);
}
if (haveSelection && i.phase == PaintActionSelection) {
//kdDebug(6040) << this << " paintSelection with startPos=" << sPos << " endPos=" << ePos << endl;
if ( sPos < ePos )
paintSelection(font, renderText(), i.p, styleToUse, tx, ty, sPos, ePos, d);
}
}
/** returns the proper ::selection pseudo style for the given element
* @return the style or 0 if no ::selection pseudo applies.
*/
inline const RenderStyle *retrieveSelectionPseudoStyle(const RenderObject *obj)
{
// http://www.w3.org/Style/CSS/Test/CSS3/Selectors/20021129/html/tests/css3-modsel-162.html
// is of the opinion that ::selection of parent elements is also to be applied
// to children, so let's do it.
while (obj) {
const RenderStyle *style = obj->style()->getPseudoStyle(RenderStyle::SELECTION);
if (style) return style;
obj = obj->parent();
}/*wend*/
return 0;
}
void InlineTextBox::paintSelection(const Font *f, RenderText *text, QPainter *p, RenderStyle* style, int tx, int ty, int startPos, int endPos, int deco)
{
if(startPos > m_len) return;
if(startPos < 0) startPos = 0;
QColor hc;
QColor hbg;
const RenderStyle* pseudoStyle = retrieveSelectionPseudoStyle(text);
if (pseudoStyle) {
// ### support outline (mandated by CSS3)
// ### support background-image? (optional by CSS3)
if (pseudoStyle->backgroundColor().isValid())
hbg = pseudoStyle->backgroundColor();
hc = pseudoStyle->color();
} else {
const QColorGroup &grp = style->palette().active();
hc = grp.highlightedText();
hbg = grp.highlight();
// ### should be at most retrieved once per render text
QColor bg = khtml::retrieveBackgroundColor(text);
// It may happen that the contrast is -- well -- virtually non existent.
// In this case, simply swap the colors, thus in compliance with
// NN4 (win32 only), IE, and Mozilla.
if (!khtml::hasSufficientContrast(hbg, bg))
qSwap(hc, hbg);
}
p->setPen(hc);
//kdDebug( 6040 ) << "textRun::painting(" << QConstString(text->str->s + m_start, m_len).string().left(30) << ") at(" << m_x+tx << "/" << m_y+ty << ")" << endl;
const bool needClipping = startPos != 0 || endPos != m_len;
if (needClipping) {
p->save();
int visualSelectionStart = f->width(text->str->s, text->str->l, m_start, startPos, m_start, m_start + m_len, m_toAdd);
int visualSelectionEnd = f->width(text->str->s, text->str->l, m_start, endPos, m_start, m_start + m_len, m_toAdd);
int visualSelectionWidth = visualSelectionEnd - visualSelectionStart;
if (m_reversed) {
visualSelectionStart = f->width(text->str->s, text->str->l, m_start, m_len) - visualSelectionEnd;
}
QRect selectionRect(m_x + tx + visualSelectionStart, m_y + ty, visualSelectionWidth, height());
QRegion r(selectionRect);
if (p->hasClipping())
r &= p->clipRegion(QPainter::CoordPainter);
p->setClipRegion(r, QPainter::CoordPainter);
}
f->drawText(p, m_x + tx, m_y + ty + m_baseline, text->str->s, text->str->l,
m_start, m_len, m_toAdd,
m_reversed ? QPainter::RTL : QPainter::LTR,
needClipping ? 0 : startPos, needClipping ? m_len : endPos,
hbg, m_y + ty, height(), deco);
if (needClipping) p->restore();
}
void InlineTextBox::paintDecoration( QPainter *pt, const Font *f, int _tx, int _ty, int deco)
{
_tx += m_x;
_ty += m_y;
if (m_truncation == cFullTruncation)
return;
int width = m_width - 1;
if (m_truncation != cNoTruncation) {
width = static_cast<RenderText*>(m_object)->width(m_start, m_truncation - m_start, m_firstLine);
}
RenderObject *p = object();
QColor underline, overline, linethrough;
p->getTextDecorationColors(deco, underline, overline, linethrough, p->style()->htmlHacks());
if(deco & UNDERLINE){
pt->setPen(underline);
f->drawDecoration(pt, _tx, _ty, baseline(), width, height(), Font::UNDERLINE);
}
if (deco & OVERLINE) {
pt->setPen(overline);
f->drawDecoration(pt, _tx, _ty, baseline(), width, height(), Font::OVERLINE);
}
if(deco & LINE_THROUGH) {
pt->setPen(linethrough);
f->drawDecoration(pt, _tx, _ty, baseline(), width, height(), Font::LINE_THROUGH);
}
// NO! Do NOT add BLINK! It is the most annouing feature of Netscape, and IE has a reason not to
// support it. Lars
}
void InlineTextBox::paintShadow(QPainter *pt, const Font *f, int _tx, int _ty, const ShadowData *shadow )
{
int x = m_x + _tx + shadow->x;
int y = m_y + _ty + shadow->y;
const RenderText* text = renderText();
if (shadow->blur <= 0) {
QColor c = pt->pen().color();
pt->setPen(shadow->color);
f->drawText(pt, x, y+m_baseline, text->str->s, text->str->l,
m_start, m_len, m_toAdd,
m_reversed ? QPainter::RTL : QPainter::LTR);
pt->setPen(c);
}
else {
const int thickness = shadow->blur;
const int w = m_width+2*thickness;
const int h = m_height+2*thickness;
const QRgb color = shadow->color.rgb();
const int gray = qGray(color);
const bool inverse = (gray < 100);
const QRgb bgColor = (inverse) ? qRgb(255,255,255) : qRgb(0,0,0);
QPixmap pixmap(w, h);
pixmap.fill(bgColor);
QPainter p;
p.begin(&pixmap);
p.setPen(shadow->color);
p.setFont(pt->font());
f->drawText(&p, thickness, thickness+m_baseline, text->str->s, text->str->l,
m_start, m_len, m_toAdd,
m_reversed ? QPainter::RTL : QPainter::LTR);
p.end();
QImage img = pixmap.convertToImage().convertDepth(32);
int md = thickness*thickness; // max-dist^2
// blur map (division cache)
float *bmap = (float*)alloca(sizeof(float)*(md+1));
for(int n=0; n<=md; n++) {
float f;
f = n/(float)(md+1);
f = 1.0 - f*f;
bmap[n] = f;
}
float factor = 0.0; // maximal potential opacity-sum
for(int n=-thickness; n<=thickness; n++)
for(int m=-thickness; m<=thickness; m++) {
int d = n*n+m*m;
if (d<=md)
factor += bmap[d];
}
// arbitratry factor adjustment to make shadows solid.
factor = factor/1.333;
// alpha map
float* amap = (float*)alloca(sizeof(float)*(h*w));
memset(amap, 0, h*w*(sizeof(float)));
for(int j=thickness; j<h-thickness; j++) {
for(int i=thickness; i<w-thickness; i++) {
QRgb col= img.pixel(i,j);
if (col == bgColor) continue;
float g = qGray(col);
if (inverse)
g = (255-g)/(255-gray);
else
g = g/gray;
for(int n=-thickness; n<=thickness; n++) {
for(int m=-thickness; m<=thickness; m++) {
int d = n*n+m*m;
if (d>md) continue;
float f = bmap[d];
amap[(i+m)+(j+n)*w] += (g*f);
}
}
}
}
QImage res(w,h,32);
res.setAlphaBuffer(true);
int r = qRed(color);
int g = qGreen(color);
int b = qBlue(color);
// divide by factor
factor = 1.0/factor;
for(int j=0; j<h; j++) {
for(int i=0; i<w; i++) {
int a = (int)(amap[i+j*w] * factor * 255.0);
if (a > 255) a = 255;
res.setPixel(i,j, qRgba(r,g,b,a));
}
}
pt->drawImage(x-thickness, y-thickness, res, 0, 0, -1, -1, Qt::DiffuseAlphaDither | Qt::ColorOnly | Qt::PreferDither);
}
// Paint next shadow effect
if (shadow->next) paintShadow(pt, f, _tx, _ty, shadow->next);
}
/**
* Distributes pixels to justify text.
* @param numSpaces spaces left, will be decremented by one
* @param toAdd number of pixels left to be distributed, will have the
* amount of pixels distributed during this call subtracted.
* @return number of pixels to distribute
*/
static inline int justifyWidth(int &numSpaces, int &toAdd) {
int a = 0;
if ( numSpaces ) {
a = toAdd/numSpaces;
toAdd -= a;
numSpaces--;
}/*end if*/
return a;
}
FindSelectionResult InlineTextBox::checkSelectionPoint(int _x, int _y, int _tx, int _ty, const Font *f, RenderText *text, int & offset, short lineHeight)
{
// kdDebug(6040) << "InlineTextBox::checkSelectionPoint " << this << " _x=" << _x << " _y=" << _y
// << " _tx+m_x=" << _tx+m_x << " _ty+m_y=" << _ty+m_y << endl;
offset = 0;
if ( _y < _ty + m_y )
return SelectionPointBefore; // above -> before
if ( _y > _ty + m_y + lineHeight ) {
// below -> after
// Set the offset to the max
offset = m_len;
return SelectionPointAfter;
}
if ( _x > _tx + m_x + m_width ) {
// to the right
return SelectionPointAfterInLine;
}
// The Y matches, check if we're on the left
if ( _x < _tx + m_x ) {
return SelectionPointBeforeInLine;
}
// consider spacing for justified text
int toAdd = m_toAdd;
bool justified = text->style()->textAlign() == JUSTIFY && toAdd > 0;
int numSpaces = 0;
if (justified) {
for( int i = 0; i < m_len; i++ )
if ( text->str->s[m_start+i].category() == QChar::Separator_Space )
numSpaces++;
}/*end if*/
int delta = _x - (_tx + m_x);
//kdDebug(6040) << "InlineTextBox::checkSelectionPoint delta=" << delta << endl;
int pos = 0;
if ( m_reversed ) {
delta -= m_width;
while(pos < m_len) {
int w = f->width( text->str->s, text->str->l, m_start + pos);
if (justified && text->str->s[m_start + pos].category() == QChar::Separator_Space)
w += justifyWidth(numSpaces, toAdd);
int w2 = w/2;
w -= w2;
delta += w2;
if(delta >= 0) break;
pos++;
delta += w;
}
} else {
while(pos < m_len) {
int w = f->width( text->str->s, text->str->l, m_start + pos);
if (justified && text->str->s[m_start + pos].category() == QChar::Separator_Space)
w += justifyWidth(numSpaces, toAdd);
int w2 = w/2;
w -= w2;
delta -= w2;
if(delta <= 0) break;
pos++;
delta -= w;
}
}
// kdDebug( 6040 ) << " Text --> inside at position " << pos << endl;
offset = pos;
return SelectionPointInside;
}
int InlineTextBox::offsetForPoint(int _x, int &ax) const
{
// Do binary search for finding out offset, saves some time for long
// runs.
int start = 0;
int end = m_len;
ax = m_x;
int offset = (start + end) / 2;
while (end - start > 0) {
// always snap to the right column. This makes up for "jumpy" vertical
// navigation.
if (end - start == 1) start = end;
offset = (start + end) / 2;
ax = m_x + widthFromStart(offset);
if (ax > _x) end = offset;
else if (ax < _x) start = offset;
else break;
}
return m_start + offset;
}
int InlineTextBox::widthFromStart(int pos) const
{
// gasp! sometimes pos is i < 0 which crashes Font::width
pos = kMax(pos, 0);
const RenderText *t = renderText();
Q_ASSERT(t->isText());
const Font *f = t->htmlFont(m_firstLine);
const QFontMetrics &fm = t->fontMetrics(m_firstLine);
int numSpaces = 0;
// consider spacing for justified text
bool justified = t->style()->textAlign() == JUSTIFY;
//kdDebug(6000) << "InlineTextBox::width(int)" << endl;
if (justified && m_toAdd > 0) do {
//kdDebug(6000) << "justify" << endl;
// QConstString cstr = QConstString(t->str->s + m_start, m_len);
for( int i = 0; i < m_len; i++ )
if ( t->str->s[m_start+i].category() == QChar::Separator_Space )
numSpaces++;
if (numSpaces == 0) break;
int toAdd = m_toAdd;
int w = 0; // accumulated width
int start = 0; // start of non-space sequence
int current = 0; // current position
while (current < pos) {
// add spacing
while (current < pos && t->str->s[m_start + current].category() == QChar::Separator_Space) {
w += f->getWordSpacing();
w += f->getLetterSpacing();
w += justifyWidth(numSpaces, toAdd);
w += fm.width(' '); // ### valid assumption? (LS)
current++; start++;
}/*wend*/
if (current >= pos) break;
// seek next space
while (current < pos && t->str->s[m_start + current].category() != QChar::Separator_Space)
current++;
// check run without spaces
if ( current > start ) {
w += f->width(t->str->s + m_start, m_len, start, current - start);
start = current;
}
}
return w;
} while(false);/*end if*/
//kdDebug(6000) << "default" << endl;
// else use existing width function
return f->width(t->str->s + m_start, m_len, 0, pos);
}
long InlineTextBox::minOffset() const
{
return m_start;
}
long InlineTextBox::maxOffset() const
{
return m_start + m_len;
}
int InlineTextBox::placeEllipsisBox(bool ltr, int blockEdge, int ellipsisWidth, bool& foundBox)
{
if (foundBox) {
m_truncation = cFullTruncation;
return -1;
}
int ellipsisX = ltr ? blockEdge - ellipsisWidth : blockEdge + ellipsisWidth;
// For LTR, if the left edge of the ellipsis is to the left of our text run, then we are the run that will get truncated.
if (ltr) {
if (ellipsisX <= m_x) {
// Too far. Just set full truncation, but return -1 and let the ellipsis just be placed at the edge of the box.
m_truncation = cFullTruncation;
foundBox = true;
return -1;
}
if (ellipsisX < m_x + m_width) {
if (m_reversed)
return -1; // FIXME: Support LTR truncation when the last run is RTL someday.
foundBox = true;
int ax;
int offset = offsetForPoint(ellipsisX, ax) - 1;
if (offset <= m_start) {
// No characters should be rendered. Set ourselves to full truncation and place the ellipsis at the min of our start
// and the ellipsis edge.
m_truncation = cFullTruncation;
return kMin(ellipsisX, (int)m_x);
}
// Set the truncation index on the text run. The ellipsis needs to be placed just after the last visible character.
m_truncation = offset;
return widthFromStart(offset - m_start);
}
}
else {
// FIXME: Support RTL truncation someday, including both modes (when the leftmost run on the line is either RTL or LTR)
}
return -1;
}
// -----------------------------------------------------------------------------
InlineTextBoxArray::InlineTextBoxArray()
{
setAutoDelete(false);
}
int InlineTextBoxArray::compareItems( Item d1, Item d2 )
{
assert(d1);
assert(d2);
return static_cast<InlineTextBox*>(d1)->m_y - static_cast<InlineTextBox*>(d2)->m_y;
}
// remove this once QVector::bsearch is fixed
int InlineTextBoxArray::findFirstMatching(Item d) const
{
int len = count();
if ( !len )
return -1;
if ( !d )
return -1;
int n1 = 0;
int n2 = len - 1;
int mid = 0;
bool found = false;
while ( n1 <= n2 ) {
int res;
mid = (n1 + n2)/2;
if ( (*this)[mid] == 0 ) // null item greater
res = -1;
else
res = ((QGVector*)this)->compareItems( d, (*this)[mid] );
if ( res < 0 )
n2 = mid - 1;
else if ( res > 0 )
n1 = mid + 1;
else { // found it
found = true;
break;
}
}
/* if ( !found )
return -1; */
// search to first one equal or bigger
while ( found && (mid > 0) && !((QGVector*)this)->compareItems(d, (*this)[mid-1]) )
mid--;
return mid;
}
// -------------------------------------------------------------------------------------
RenderText::RenderText(DOM::NodeImpl* node, DOMStringImpl *_str)
: RenderObject(node)
{
// init RenderObject attributes
setRenderText(); // our object inherits from RenderText
m_minWidth = -1;
m_maxWidth = -1;
str = _str;
if(str) str->ref();
KHTMLAssert(!str || !str->l || str->s);
m_selectionState = SelectionNone;
m_hasReturn = true;
#ifdef DEBUG_LAYOUT
QConstString cstr(str->s, str->l);
kdDebug( 6040 ) << "RenderText ctr( "<< cstr.string().length() << " ) '" << cstr.string() << "'" << endl;
#endif
}
void RenderText::setStyle(RenderStyle *_style)
{
if ( style() != _style ) {
bool changedText = ((!style() && ( _style->textTransform() != TTNONE ||
!_style->preserveLF() || !_style->preserveWS() )) ||
(style() && (style()->textTransform() != _style->textTransform() ||
style()->whiteSpace() != _style->whiteSpace())));
RenderObject::setStyle( _style );
m_lineHeight = RenderObject::lineHeight(false);
if (!isBR() && changedText) {
DOM::DOMStringImpl* textToTransform = originalString();
if (textToTransform)
setText(textToTransform, true);
}
}
}
RenderText::~RenderText()
{
KHTMLAssert(m_lines.count() == 0);
if(str) str->deref();
}
void RenderText::deleteInlineBoxes(RenderArena* arena)
{
// this is a slight variant of QArray::clear().
// We don't delete the array itself here because its
// likely to be used in the same size later again, saves
// us resize() calls
unsigned int len = m_lines.size();
if (len) {
if (!arena)
arena = renderArena();
for(unsigned int i=0; i < len; i++) {
InlineTextBox* s = m_lines.at(i);
if (s)
s->detach(arena);
m_lines.remove(i);
}
}
KHTMLAssert(m_lines.count() == 0);
}
bool RenderText::isTextFragment() const
{
return false;
}
DOM::DOMStringImpl* RenderText::originalString() const
{
return element() ? element()->string() : 0;
}
InlineTextBox * RenderText::findInlineTextBox( int offset, int &pos, bool checkFirstLetter )
{
// The text boxes point to parts of the rendertext's str string
// (they don't include '\n')
// Find the text box that includes the character at @p offset
// and return pos, which is the position of the char in the run.
// FIXME: make this use binary search? Dirk says it won't work :-( (LS)
(void)checkFirstLetter;
#if 0
if (checkFirstLetter && forcedMinOffset()) {
// kdDebug(6040) << "checkFirstLetter: forcedMinOffset: " << forcedMinOffset() << endl;
RenderFlow *firstLetter = static_cast<RenderFlow *>(previousSibling());
if (firstLetter && firstLetter->isFlow() && firstLetter->isFirstLetter()) {
RenderText *letterText = static_cast<RenderText *>(firstLetter->firstChild());
//kdDebug(6040) << "lettertext: " << letterText << " minOfs: " << letterText->minOffset() << " maxOfs: " << letterText->maxOffset() << endl;
if (offset >= letterText->minOffset() && offset <= letterText->maxOffset()) {
InlineTextBox *result = letterText->findInlineTextBox(offset, pos, false);
//kdDebug(6040) << "result: " << result << endl;
if (result) return result;
}
}
}
#endif
if ( m_lines.isEmpty() )
return 0L;
// The text boxes don't resemble a contiguous coverage of the text, there
// may be holes. Therefore, we snap to the nearest previous text box if
// the given offset happens to point to such a hole.
InlineTextBox* s = m_lines[0];
uint count = m_lines.count();
uint si = 0;
uint nearest_idx = 0; // index of nearest text box
int nearest = INT_MAX; // nearest distance
//kdDebug(6040) << "s[" << si << "] m_start " << s->m_start << " m_end " << (s->m_start + s->m_len) << endl;
while(!(offset >= s->m_start && offset <= s->m_start + s->m_len)
&& ++si < count)
{
int dist = offset - (s->m_start + s->m_len);
//kdDebug(6040) << "dist " << dist << " nearest " << nearest << endl;
if (dist >= 0 && dist <= nearest) {
nearest = dist;
nearest_idx = si - 1;
}/*end if*/
s = m_lines[si];
//kdDebug(6040) << "s[" << si << "] m_start " << s->m_start << " m_end " << (s->m_start + s->m_len) << endl;
}
//kdDebug(6040) << "nearest_idx " << nearest_idx << " count " << count << endl;
if (si >= count) s = m_lines[nearest_idx];
// we are now in the correct text box
pos = kMin(offset - s->m_start, int( s->m_len ));
//kdDebug(6040) << "offset=" << offset << " s->m_start=" << s->m_start << endl;
return s;
}
bool RenderText::nodeAtPoint(NodeInfo& info, int _x, int _y, int _tx, int _ty, HitTestAction /*hitTestAction*/, bool /*inBox*/)
{
assert(parent());
bool inside = false;
if (style()->visibility() != HIDDEN) {
InlineTextBox *s = m_lines.count() ? m_lines[0] : 0;
int si = 0;
while(s) {
if((_y >=_ty + s->m_y) && (_y < _ty + s->m_y + s->m_height) &&
(_x >= _tx + s->m_x) && (_x <_tx + s->m_x + s->m_width) ) {
inside = true;
break;
}
s = si < (int) m_lines.count()-1 ? m_lines[++si] : 0;
}
}
// #### ported over from Safari. Can this happen at all? (lars)
if (inside && element()) {
if (info.innerNode() && info.innerNode()->renderer() &&
!info.innerNode()->renderer()->isInline()) {
// Within the same layer, inlines are ALWAYS fully above blocks. Change inner node.
info.setInnerNode(element());
// Clear everything else.
info.setInnerNonSharedNode(0);
info.setURLElement(0);
}
if (!info.innerNode())
info.setInnerNode(element());
if(!info.innerNonSharedNode())
info.setInnerNonSharedNode(element());
}
return inside;
}
FindSelectionResult RenderText::checkSelectionPoint(int _x, int _y, int _tx, int _ty, DOM::NodeImpl*& node, int &offset, SelPointState &)
{
// kdDebug(6040) << "RenderText::checkSelectionPoint " << this << " _x=" << _x << " _y=" << _y
// << " _tx=" << _tx << " _ty=" << _ty << endl;
//kdDebug(6040) << renderName() << "::checkSelectionPoint x=" << xPos() << " y=" << yPos() << " w=" << width() << " h=" << height() << " m_lines.count=" << m_lines.count() << endl;
NodeImpl *lastNode = 0;
int lastOffset = 0;
FindSelectionResult lastResult = SelectionPointAfter;
for(unsigned int si = 0; si < m_lines.count(); si++)
{
InlineTextBox* s = m_lines[si];
FindSelectionResult result;
const Font *f = htmlFont( si==0 );
result = s->checkSelectionPoint(_x, _y, _tx, _ty, f, this, offset, m_lineHeight);
// kdDebug(6040) << "RenderText::checkSelectionPoint " << this << " line " << si << " result=" << result << " offset=" << offset << endl;
if ( result == SelectionPointInside ) // x,y is inside the textrun
{
offset += s->m_start; // add the offset from the previous lines
// kdDebug(6040) << "RenderText::checkSelectionPoint inside -> " << offset << endl;
node = element();
return SelectionPointInside;
} else if ( result == SelectionPointBefore ) {
if (!lastNode) {
// x,y is before the textrun -> stop here
offset = 0;
// kdDebug(6040) << "RenderText::checkSelectionPoint " << this << "before us -> returning Before" << endl;
node = element();
return SelectionPointBefore;
}
} else if ( result == SelectionPointBeforeInLine ) {
offset = s->m_start;
node = element();
return SelectionPointInside;
} else if ( result == SelectionPointAfterInLine ) {
lastOffset = s->m_start + s->m_len;
lastNode = element();
lastResult = result;
// no return here
}
}
if (lastNode) {
offset = lastOffset;
node = lastNode;
// kdDebug(6040) << "RenderText::checkSelectionPoint: lastNode " << lastNode << " lastOffset " << lastOffset << endl;
return lastResult;
}
// set offset to max
offset = str->l;
//qDebug("setting node to %p", element());
node = element();
// kdDebug(6040) << "RenderText::checkSelectionPoint: node " << node << " offset " << offset << endl;
return SelectionPointAfter;
}
void RenderText::caretPos(int offset, int flags, int &_x, int &_y, int &width, int &height)
{
if (!m_lines.count()) {
_x = _y = height = -1;
width = 1;
return;
}
int pos;
InlineTextBox * s = findInlineTextBox( offset, pos, true );
RenderText *t = s->renderText();
// kdDebug(6040) << "offset="<<offset << " pos="<<pos << endl;
const QFontMetrics &fm = t->metrics( s->m_firstLine );
height = fm.height(); // s->m_height;
_x = s->m_x + s->widthFromStart(pos);
_y = s->m_y + s->baseline() - fm.ascent();
width = 1;
if (flags & CFOverride) {
width = offset < maxOffset() ? fm.width(str->s[offset]) : 1;
}/*end if*/
#if 0
kdDebug(6040) << "_x="<<_x << " s->m_x="<<s->m_x
<< " s->m_start"<<s->m_start
<< " s->m_len" << s->m_len << " _y=" << _y << endl;
#endif
int absx, absy;
if (absolutePosition(absx,absy))
{
//kdDebug(6040) << "absx=" << absx << " absy=" << absy << endl;
_x += absx;
_y += absy;
} else {
// we don't know our absolute position, and there is no point returning
// just a relative one
_x = _y = -1;
}
}
long RenderText::minOffset() const
{
if (!m_lines.count()) return 0;
// FIXME: it is *not* guaranteed that the first run contains the lowest offset
// Either make this a linear search (slow),
// or maintain an index (needs much mem),
// or calculate and store it in bidi.cpp (needs calculation even if not needed)
// (LS)
return m_lines[0]->m_start;
}
long RenderText::maxOffset() const
{
int count = m_lines.count();
if (!count) return str->l;
// FIXME: it is *not* guaranteed that the last run contains the highest offset
// Either make this a linear search (slow),
// or maintain an index (needs much mem),
// or calculate and store it in bidi.cpp (needs calculation even if not needed)
// (LS)
return m_lines[count - 1]->m_start + m_lines[count - 1]->m_len;
}
bool RenderText::absolutePosition(int &xPos, int &yPos, bool) const
{
return RenderObject::absolutePosition(xPos, yPos, false);
}
bool RenderText::posOfChar(int chr, int &x, int &y)
{
if (!parent())
return false;
parent()->absolutePosition( x, y, false );
int pos;
InlineTextBox * s = findInlineTextBox( chr, pos );
if ( s ) {
// s is the line containing the character
x += s->m_x; // this is the x of the beginning of the line, but it's good enough for now
y += s->m_y;
return true;
}
return false;
}
void RenderText::paint( PaintInfo& /*pI*/, int /*tx*/, int /*ty*/)
{
KHTMLAssert( false );
}
void RenderText::calcMinMaxWidth()
{
KHTMLAssert( !minMaxKnown() );
// ### calc Min and Max width...
m_minWidth = m_beginMinWidth = m_endMinWidth = 0;
m_maxWidth = 0;
if (isBR())
return;
int currMinWidth = 0;
int currMaxWidth = 0;
m_hasBreakableChar = m_hasBreak = m_hasBeginWS = m_hasEndWS = false;
// ### not 100% correct for first-line
const Font *f = htmlFont( false );
int wordSpacing = style()->wordSpacing();
int len = str->l;
bool isSpace = false;
bool firstWord = true;
bool firstLine = true;
for(int i = 0; i < len; i++)
{
unsigned short c = str->s[i].unicode();
bool isNewline = false;
// If line-breaks survive to here they are preserved
if ( c == '\n' ) {
m_hasBreak = true;
isNewline = true;
isSpace = false;
} else
isSpace = c == ' ';
if ((isSpace || isNewline) && i == 0)
m_hasBeginWS = true;
if ((isSpace || isNewline) && i == len-1)
m_hasEndWS = true;
if (i && c == SOFT_HYPHEN)
continue;
int wordlen = 0;
while( i+wordlen < len && (i+wordlen == 0 || str->s[i+wordlen].unicode() != SOFT_HYPHEN) &&
!(isBreakable( str->s, i+wordlen, str->l )) )
wordlen++;
if (wordlen)
{
#ifndef APPLE_CHANGES
int w = f->width(str->s, str->l, i, wordlen);
#else
int w = widthFromCache(f, i, wordlen);
#endif
currMinWidth += w;
currMaxWidth += w;
// Add in wordspacing to our maxwidth, but not if this is the last word.
if (wordSpacing && !containsOnlyWhitespace(i+wordlen, len-(i+wordlen)))
currMaxWidth += wordSpacing;
if (firstWord) {
firstWord = false;
m_beginMinWidth = w;
}
m_endMinWidth = w;
if(currMinWidth > m_minWidth) m_minWidth = currMinWidth;
currMinWidth = 0;
i += wordlen-1;
}
else {
// Nowrap can never be broken, so don't bother setting the
// breakable character boolean. Pre can only be broken if we encounter a newline.
if (style()->autoWrap() || isNewline)
m_hasBreakableChar = true;
if(currMinWidth > m_minWidth) m_minWidth = currMinWidth;
currMinWidth = 0;
if (isNewline) // Only set if isPre was true and we saw a newline.
{
if ( firstLine ) {
firstLine = false;
m_beginMinWidth = currMaxWidth;
}
if(currMaxWidth > m_maxWidth) m_maxWidth = currMaxWidth;
currMaxWidth = 0;
}
else
{
currMaxWidth += f->width( str->s, str->l, i + wordlen );
}
}
}
if(currMinWidth > m_minWidth) m_minWidth = currMinWidth;
if(currMaxWidth > m_maxWidth) m_maxWidth = currMaxWidth;
if (!style()->autoWrap()) {
m_minWidth = m_maxWidth;
if (style()->preserveLF()) {
if (firstLine)
m_beginMinWidth = m_maxWidth;
m_endMinWidth = currMaxWidth;
}
}
setMinMaxKnown();
//kdDebug( 6040 ) << "Text::calcMinMaxWidth(): min = " << m_minWidth << " max = " << m_maxWidth << endl;
}
int RenderText::minXPos() const
{
if (!m_lines.count())
return 0;
int retval=6666666;
for (unsigned i=0;i < m_lines.count(); i++)
{
retval = kMin ( retval, int( m_lines[i]->m_x ));
}
return retval;
}
int RenderText::inlineXPos() const
{
return minXPos();
}
int RenderText::inlineYPos() const
{
return m_lines.isEmpty() ? 0 : m_lines[0]->yPos();
}
const QFont &RenderText::font()
{
return style()->font();
}
void RenderText::setText(DOMStringImpl *text, bool force)
{
if( !force && str == text ) return;
DOMStringImpl *oldstr = str;
if(text && style())
str = text->collapseWhiteSpace(style()->preserveLF(), style()->preserveWS());
else
str = text;
if(str) str->ref();
if(oldstr) oldstr->deref();
if ( str && style() ) {
oldstr = str;
switch(style()->textTransform()) {
case CAPITALIZE:
{
RenderObject *o;
bool runOnString = false;
// find previous non-empty text renderer if one exists
for (o = previousRenderer(); o; o = o->previousRenderer()) {
if (!o->isInlineFlow()) {
if (!o->isText())
break;
DOMStringImpl *prevStr = static_cast<RenderText*>(o)->string();
// !prevStr can happen with css like "content:open-quote;"
if (!prevStr)
break;
if (prevStr->length() == 0)
continue;
QChar c = (*prevStr)[prevStr->length() - 1];
if (!c.isSpace())
runOnString = true;
break;
}
}
str = str->capitalize(runOnString);
}
break;
case UPPERCASE: str = str->upper(); break;
case LOWERCASE: str = str->lower(); break;
case NONE:
default:;
}
str->ref();
oldstr->deref();
}
// ### what should happen if we change the text of a
// RenderBR object ?
KHTMLAssert(!isBR() || (str->l == 1 && (*str->s) == '\n'));
KHTMLAssert(!str->l || str->s);
setNeedsLayoutAndMinMaxRecalc();
#ifdef BIDI_DEBUG
QConstString cstr(str->s, str->l);
kdDebug( 6040 ) << "RenderText::setText( " << cstr.string().length() << " ) '" << cstr.string() << "'" << endl;
#endif
}
int RenderText::height() const
{
int retval;
if ( m_lines.count() )
retval = m_lines[m_lines.count()-1]->m_y + m_lineHeight - m_lines[0]->m_y;
else
retval = metrics( false ).height();
return retval;
}
short RenderText::lineHeight( bool firstLine ) const
{
if ( firstLine )
return RenderObject::lineHeight( firstLine );
return m_lineHeight;
}
short RenderText::baselinePosition( bool firstLine ) const
{
const QFontMetrics &fm = metrics( firstLine );
return fm.ascent() +
( lineHeight( firstLine ) - fm.height() ) / 2;
}
InlineBox* RenderText::createInlineBox(bool, bool isRootLineBox)
{
KHTMLAssert( !isRootLineBox );
return new (renderArena()) InlineTextBox(this);
}
void RenderText::position(InlineBox* box, int from, int len, bool reverse)
{
//kdDebug(6040) << "position: from="<<from<<" len="<<len<<endl;
// ### should not be needed!!!
// asserts sometimes with pre (that unibw-hamburg testcase). ### find out why
//KHTMLAssert(!(len == 0 || (str->l && len == 1 && *(str->s+from) == '\n') ));
// It is now needed. BRs need text boxes too otherwise caret navigation
// gets stuck (LS)
// if (len == 0 || (str->l && len == 1 && *(str->s+from) == '\n') ) return;
reverse = reverse && !style()->visuallyOrdered();
#ifdef DEBUG_LAYOUT
QChar *ch = str->s+from;
QConstString cstr(ch, len);
#endif
KHTMLAssert(box->isInlineTextBox());
InlineTextBox *s = static_cast<InlineTextBox *>(box);
s->m_start = from;
s->m_len = len;
s->m_reversed = reverse;
//kdDebug(6040) << "m_start: " << s->m_start << " m_len: " << s->m_len << endl;
if(m_lines.count() == m_lines.size())
m_lines.resize(m_lines.size()*2+1);
m_lines.insert(m_lines.count(), s);
//kdDebug(6040) << this << " " << renderName() << "::position inserted" << endl;
}
unsigned int RenderText::width(unsigned int from, unsigned int len, bool firstLine) const
{
if(!str->s || from > str->l ) return 0;
if ( from + len > str->l ) len = str->l - from;
const Font *f = htmlFont( firstLine );
return width( from, len, f );
}
unsigned int RenderText::width(unsigned int from, unsigned int len, const Font *f) const
{
if(!str->s || from > str->l ) return 0;
if ( from + len > str->l ) len = str->l - from;
if ( f == &style()->htmlFont() && from == 0 && len == str->l )
return m_maxWidth;
int w = f->width(str->s, str->l, from, len );
//kdDebug( 6040 ) << "RenderText::width(" << from << ", " << len << ") = " << w << endl;
return w;
}
short RenderText::width() const
{
int w;
int minx = 100000000;
int maxx = 0;
// slooow
for(unsigned int si = 0; si < m_lines.count(); si++) {
InlineTextBox* s = m_lines[si];
if(s->m_x < minx)
minx = s->m_x;
if(s->m_x + s->m_width > maxx)
maxx = s->m_x + s->m_width;
}
w = kMax(0, maxx-minx);
return w;
}
void RenderText::repaint(Priority p)
{
RenderObject *cb = containingBlock();
if(cb)
cb->repaint(p);
}
bool RenderText::isFixedWidthFont() const
{
return QFontInfo(style()->font()).fixedPitch();
}
short RenderText::verticalPositionHint( bool firstLine ) const
{
return parent()->verticalPositionHint( firstLine );
}
const QFontMetrics &RenderText::metrics(bool firstLine) const
{
if( firstLine && hasFirstLine() ) {
RenderStyle *pseudoStyle = style()->getPseudoStyle(RenderStyle::FIRST_LINE);
if ( pseudoStyle )
return pseudoStyle->fontMetrics();
}
return style()->fontMetrics();
}
const Font *RenderText::htmlFont(bool firstLine) const
{
const Font *f = 0;
if( firstLine && hasFirstLine() ) {
RenderStyle *pseudoStyle = style()->getPseudoStyle(RenderStyle::FIRST_LINE);
if ( pseudoStyle )
f = &pseudoStyle->htmlFont();
} else {
f = &style()->htmlFont();
}
return f;
}
bool RenderText::containsOnlyWhitespace(unsigned int from, unsigned int len) const
{
unsigned int currPos;
for (currPos = from;
currPos < from+len && (str->s[currPos] == '\n' || str->s[currPos].direction() == QChar::DirWS);
currPos++);
return currPos >= (from+len);
}
void RenderText::trimmedMinMaxWidth(short& beginMinW, bool& beginWS,
short& endMinW, bool& endWS,
bool& hasBreakableChar, bool& hasBreak,
short& beginMaxW, short& endMaxW,
short& minW, short& maxW, bool& stripFrontSpaces)
{
bool preserveWS = style()->preserveWS();
bool preserveLF = style()->preserveLF();
bool autoWrap = style()->autoWrap();
if (preserveWS)
stripFrontSpaces = false;
int len = str->l;
if (len == 0 || (stripFrontSpaces && str->containsOnlyWhitespace())) {
maxW = 0;
hasBreak = false;
return;
}
minW = m_minWidth;
maxW = m_maxWidth;
beginWS = stripFrontSpaces ? false : m_hasBeginWS;
endWS = m_hasEndWS;
beginMinW = m_beginMinWidth;
endMinW = m_endMinWidth;
hasBreakableChar = m_hasBreakableChar;
hasBreak = m_hasBreak;
if (stripFrontSpaces && (str->s[0].direction() == QChar::DirWS || (!preserveLF && str->s[0] == '\n'))) {
const Font *f = htmlFont( false );
QChar space[1]; space[0] = ' ';
int spaceWidth = f->width(space, 1, 0);
maxW -= spaceWidth;
}
stripFrontSpaces = !preserveWS && m_hasEndWS;
if (!autoWrap)
minW = maxW;
else if (minW > maxW)
minW = maxW;
// Compute our max widths by scanning the string for newlines.
if (hasBreak) {
const Font *f = htmlFont( false );
bool firstLine = true;
beginMaxW = endMaxW = maxW;
for(int i = 0; i < len; i++)
{
int linelen = 0;
while( i+linelen < len && str->s[i+linelen] != '\n')
linelen++;
if (linelen)
{
#ifndef APPLE_CHANGES
endMaxW = f->width(str->s, str->l, i, linelen);
#else
endMaxW = widthFromCache(f, i, linelen);
#endif
if (firstLine) {
firstLine = false;
beginMaxW = endMaxW;
}
i += linelen;
}
else if (firstLine) {
beginMaxW = 0;
firstLine = false;
}
if (i == len-1)
// A <pre> run that ends with a newline, as in, e.g.,
// <pre>Some text\n\n<span>More text</pre>
endMaxW = 0;
}
}
}
#ifdef ENABLE_DUMP
static QString quoteAndEscapeNonPrintables(const QString &s)
{
QString result;
result += '"';
for (uint i = 0; i != s.length(); ++i) {
QChar c = s.at(i);
if (c == '\\') {
result += "\\\\";
} else if (c == '"') {
result += "\\\"";
} else {
ushort u = c.unicode();
if (u >= 0x20 && u < 0x7F) {
result += c;
} else {
QString hex;
hex.sprintf("\\x{%X}", u);
result += hex;
}
}
}
result += '"';
return result;
}
static void writeTextRun(QTextStream &ts, const RenderText &o, const InlineTextBox &run)
{
ts << "text run at (" << run.m_x << "," << run.m_y << ") width " << run.m_width << ": "
<< quoteAndEscapeNonPrintables(o.data().string().mid(run.m_start, run.m_len));
}
void RenderText::dump(QTextStream &stream, const QString &ind) const
{
RenderObject::dump( stream, ind );
for (unsigned int i = 0; i < m_lines.count(); i++) {
stream << endl << ind << " ";
writeTextRun(stream, *this, *m_lines[i]);
}
}
#endif
RenderTextFragment::RenderTextFragment(DOM::NodeImpl* _node, DOM::DOMStringImpl* _str,
int startOffset, int endOffset)
:RenderText(_node, _str->substring(startOffset, endOffset)),
m_start(startOffset), m_end(endOffset), m_generatedContentStr(0)
{}
RenderTextFragment::RenderTextFragment(DOM::NodeImpl* _node, DOM::DOMStringImpl* _str)
:RenderText(_node, _str), m_start(0)
{
m_generatedContentStr = _str;
if (_str) {
_str->ref();
m_end = _str->l;
}
else
m_end = 0;
}
RenderTextFragment::~RenderTextFragment()
{
if (m_generatedContentStr)
m_generatedContentStr->deref();
}
bool RenderTextFragment::isTextFragment() const
{
return true;
}
DOM::DOMStringImpl* RenderTextFragment::originalString() const
{
DOM::DOMStringImpl* result = 0;
if (element())
result = element()->string();
else
result = contentString();
if (result && (start() > 0 || start() < result->l))
result = result->substring(start(), end());
return result;
}
#undef BIDI_DEBUG
#undef DEBUG_LAYOUT