diff options
Diffstat (limited to 'WebCore/rendering/SVGInlineTextBox.cpp')
-rw-r--r-- | WebCore/rendering/SVGInlineTextBox.cpp | 547 |
1 files changed, 547 insertions, 0 deletions
diff --git a/WebCore/rendering/SVGInlineTextBox.cpp b/WebCore/rendering/SVGInlineTextBox.cpp new file mode 100644 index 0000000..eb1baec --- /dev/null +++ b/WebCore/rendering/SVGInlineTextBox.cpp @@ -0,0 +1,547 @@ +/** + * This file is part of the DOM implementation for KDE. + * + * Copyright (C) 2007 Rob Buis <buis@kde.org> + * (C) 2007 Nikolas Zimmermann <zimmermann@kde.org> + * + * 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. + * + */ + +#include "config.h" + +#if ENABLE(SVG) +#include "SVGInlineTextBox.h" + +#include "Document.h" +#include "Editor.h" +#include "Frame.h" +#include "GraphicsContext.h" +#include "InlineFlowBox.h" +#include "Range.h" +#include "SVGPaintServer.h" +#include "SVGRootInlineBox.h" +#include "Text.h" + +#include <float.h> + +using std::max; + +namespace WebCore { + +SVGInlineTextBox::SVGInlineTextBox(RenderObject* obj) + : InlineTextBox(obj) +{ +} + +int SVGInlineTextBox::selectionTop() +{ + return m_y; +} + +int SVGInlineTextBox::selectionHeight() +{ + return m_height; +} + +SVGRootInlineBox* SVGInlineTextBox::svgRootInlineBox() const +{ + // Find associated root inline box + InlineFlowBox* parentBox = parent(); + + while (parentBox && !parentBox->isRootInlineBox()) + parentBox = parentBox->parent(); + + ASSERT(parentBox); + ASSERT(parentBox->isRootInlineBox()); + + if (!parentBox->isSVGRootInlineBox()) + return 0; + + return static_cast<SVGRootInlineBox*>(parentBox); +} + +float SVGInlineTextBox::calculateGlyphWidth(RenderStyle* style, int offset) const +{ + ASSERT(style); + return style->font().floatWidth(svgTextRunForInlineTextBox(textObject()->text()->characters() + offset, 1, style, this, 0)); +} + +float SVGInlineTextBox::calculateGlyphHeight(RenderStyle* style, int offset) const +{ + ASSERT(style); + + // This is just a guess, and the only purpose of this function is to centralize this hack. + // In real-life top-top-bottom scripts this won't be enough, I fear. + return style->font().ascent() + style->font().descent(); +} + +FloatRect SVGInlineTextBox::calculateGlyphBoundaries(RenderStyle* style, int offset, const SVGChar& svgChar) const +{ + const Font& font = style->font(); + + // Take RTL text into account and pick right glyph width/height. + float glyphWidth = 0.0f; + + if (!m_reversed) + glyphWidth = calculateGlyphWidth(style, offset); + else + glyphWidth = calculateGlyphWidth(style, start() + end() - offset); + + float x1 = svgChar.x; + float x2 = svgChar.x + glyphWidth; + + float y1 = svgChar.y - font.ascent(); + float y2 = svgChar.y + font.descent(); + + FloatRect glyphRect(x1, y1, x2 - x1, y2 - y1); + + // Take per-character transformations into account + AffineTransform ctm = svgChar.characterTransform(); + if (!ctm.isIdentity()) + glyphRect = ctm.mapRect(glyphRect); + + return glyphRect; +} + +// Helper class for closestCharacterToPosition() +struct SVGInlineTextBoxClosestCharacterToPositionWalker { + SVGInlineTextBoxClosestCharacterToPositionWalker(int x, int y) + : m_character(0) + , m_distance(FLT_MAX) + , m_x(x) + , m_y(y) + , m_offset(0) + { + } + + void chunkPortionCallback(SVGInlineTextBox* textBox, int startOffset, const AffineTransform& chunkCtm, + const Vector<SVGChar>::iterator& start, const Vector<SVGChar>::iterator& end) + { + RenderStyle* style = textBox->textObject()->style(); + + Vector<SVGChar>::iterator closestCharacter = 0; + unsigned int closestOffset = UINT_MAX; + + for (Vector<SVGChar>::iterator it = start; it != end; ++it) { + if (it->isHidden()) + continue; + + unsigned int newOffset = textBox->start() + (it - start) + startOffset; + FloatRect glyphRect = chunkCtm.mapRect(textBox->calculateGlyphBoundaries(style, newOffset, *it)); + + // Take RTL text into account and pick right glyph width/height. + // NOTE: This offset has to be corrected _after_ calling calculateGlyphBoundaries + if (textBox->m_reversed) + newOffset = textBox->start() + textBox->end() - newOffset; + + // Calculate distances relative to the glyph mid-point. I hope this is accurate enough. + float xDistance = glyphRect.x() + glyphRect.width() / 2.0f - m_x; + float yDistance = glyphRect.y() - glyphRect.height() / 2.0f - m_y; + + float newDistance = sqrtf(xDistance * xDistance + yDistance * yDistance); + if (newDistance <= m_distance) { + m_distance = newDistance; + closestOffset = newOffset; + closestCharacter = it; + } + } + + if (closestOffset != UINT_MAX) { + // Record current chunk, if it contains the current closest character next to the mouse. + m_character = closestCharacter; + m_offset = closestOffset; + } + } + + SVGChar* character() const + { + return m_character; + } + + int offset() const + { + if (!m_character) + return 0; + + return m_offset; + } + +private: + Vector<SVGChar>::iterator m_character; + float m_distance; + + int m_x; + int m_y; + int m_offset; +}; + +// Helper class for selectionRect() +struct SVGInlineTextBoxSelectionRectWalker { + SVGInlineTextBoxSelectionRectWalker() + { + } + + void chunkPortionCallback(SVGInlineTextBox* textBox, int startOffset, const AffineTransform& chunkCtm, + const Vector<SVGChar>::iterator& start, const Vector<SVGChar>::iterator& end) + { + RenderStyle* style = textBox->textObject()->style(); + + for (Vector<SVGChar>::iterator it = start; it != end; ++it) { + if (it->isHidden()) + continue; + + unsigned int newOffset = textBox->start() + (it - start) + startOffset; + m_selectionRect.unite(textBox->calculateGlyphBoundaries(style, newOffset, *it)); + } + + m_selectionRect = chunkCtm.mapRect(m_selectionRect); + } + + FloatRect selectionRect() const + { + return m_selectionRect; + } + +private: + FloatRect m_selectionRect; +}; + +SVGChar* SVGInlineTextBox::closestCharacterToPosition(int x, int y, int& offset) const +{ + SVGRootInlineBox* rootBox = svgRootInlineBox(); + if (!rootBox) + return 0; + + SVGInlineTextBoxClosestCharacterToPositionWalker walkerCallback(x, y); + SVGTextChunkWalker<SVGInlineTextBoxClosestCharacterToPositionWalker> walker(&walkerCallback, &SVGInlineTextBoxClosestCharacterToPositionWalker::chunkPortionCallback); + + rootBox->walkTextChunks(&walker, this); + + offset = walkerCallback.offset(); + return walkerCallback.character(); +} + +bool SVGInlineTextBox::svgCharacterHitsPosition(int x, int y, int& offset) const +{ + SVGChar* charAtPosPtr = closestCharacterToPosition(x, y, offset); + if (!charAtPosPtr) + return false; + + SVGChar& charAtPos = *charAtPosPtr; + RenderStyle* style = textObject()->style(m_firstLine); + FloatRect glyphRect = calculateGlyphBoundaries(style, offset, charAtPos); + + if (m_reversed) + offset++; + + // FIXME: todo list + // (#13910) This code does not handle bottom-to-top/top-to-bottom vertical text. + + // Check whether y position hits the current character + if (y < charAtPos.y - glyphRect.height() || y > charAtPos.y) + return false; + + // Check whether x position hits the current character + if (x < charAtPos.x) { + if (offset > 0 && !m_reversed) + return true; + else if (offset < (int) end() && m_reversed) + return true; + + return false; + } + + // If we are past the last glyph of this box, don't mark it as 'hit' anymore. + if (x >= charAtPos.x + glyphRect.width() && offset == (int) end()) + return false; + + // Snap to character at half of it's advance + if (x >= charAtPos.x + glyphRect.width() / 2.0) + offset += m_reversed ? -1 : 1; + + return true; +} + +int SVGInlineTextBox::offsetForPosition(int x, bool includePartialGlyphs) const +{ + // SVG doesn't use the offset <-> position selection system. + ASSERT_NOT_REACHED(); + return 0; +} + +int SVGInlineTextBox::positionForOffset(int offset) const +{ + // SVG doesn't use the offset <-> position selection system. + ASSERT_NOT_REACHED(); + return 0; +} + +bool SVGInlineTextBox::nodeAtPoint(const HitTestRequest& request, HitTestResult& result, int x, int y, int tx, int ty) +{ + ASSERT(!isLineBreak()); + + IntRect rect = selectionRect(0, 0, 0, len()); + if (object()->style()->visibility() == VISIBLE && rect.contains(x, y)) { + object()->updateHitTestResult(result, IntPoint(x - tx, y - ty)); + return true; + } + + return false; +} + +IntRect SVGInlineTextBox::selectionRect(int, int, int startPos, int endPos) +{ + if (startPos >= endPos) + return IntRect(); + + // TODO: Actually respect startPos/endPos - we're returning the _full_ selectionRect + // here. This won't lead to visible bugs, but to extra work being done. Investigate. + SVGRootInlineBox* rootBox = svgRootInlineBox(); + if (!rootBox) + return IntRect(); + + SVGInlineTextBoxSelectionRectWalker walkerCallback; + SVGTextChunkWalker<SVGInlineTextBoxSelectionRectWalker> walker(&walkerCallback, &SVGInlineTextBoxSelectionRectWalker::chunkPortionCallback); + + rootBox->walkTextChunks(&walker, this); + return enclosingIntRect(walkerCallback.selectionRect()); +} + +void SVGInlineTextBox::paintCharacters(RenderObject::PaintInfo& paintInfo, int tx, int ty, const SVGChar& svgChar, const UChar* chars, int length, SVGPaintServer* activePaintServer) +{ + if (object()->style()->visibility() != VISIBLE || paintInfo.phase == PaintPhaseOutline) + return; + + ASSERT(paintInfo.phase != PaintPhaseSelfOutline && paintInfo.phase != PaintPhaseChildOutlines); + + RenderText* text = textObject(); + ASSERT(text); + + bool isPrinting = text->document()->printing(); + + // Determine whether or not we're selected. + bool haveSelection = !isPrinting && selectionState() != RenderObject::SelectionNone; + if (!haveSelection && paintInfo.phase == PaintPhaseSelection) + // When only painting the selection, don't bother to paint if there is none. + return; + + // Determine whether or not we have a composition. + bool containsComposition = text->document()->frame()->editor()->compositionNode() == text->node(); + bool useCustomUnderlines = containsComposition && text->document()->frame()->editor()->compositionUsesCustomUnderlines(); + + // Set our font + RenderStyle* styleToUse = text->style(isFirstLineStyle()); + const Font* font = &styleToUse->font(); + if (*font != paintInfo.context->font()) + paintInfo.context->setFont(*font); + + AffineTransform ctm = svgChar.characterTransform(); + if (!ctm.isIdentity()) + paintInfo.context->concatCTM(ctm); + + // 1. Paint backgrounds behind text if needed. Examples of such backgrounds include selection + // and marked text. + if (paintInfo.phase != PaintPhaseSelection && !isPrinting) { +#if PLATFORM(MAC) + // Custom highlighters go behind everything else. + if (styleToUse->highlight() != nullAtom && !paintInfo.context->paintingDisabled()) + paintCustomHighlight(tx, ty, styleToUse->highlight()); +#endif + + if (containsComposition && !useCustomUnderlines) + paintCompositionBackground(paintInfo.context, tx, ty, styleToUse, font, + text->document()->frame()->editor()->compositionStart(), + text->document()->frame()->editor()->compositionEnd()); + + paintDocumentMarkers(paintInfo.context, tx, ty, styleToUse, font, true); + + if (haveSelection && !useCustomUnderlines) { + int boxStartOffset = chars - text->characters() - start(); + paintSelection(boxStartOffset, svgChar, chars, length, paintInfo.context, styleToUse, font); + } + } + + // Set a text shadow if we have one. + // FIXME: Support multiple shadow effects. Need more from the CG API before + // we can do this. + bool setShadow = false; + if (styleToUse->textShadow()) { + paintInfo.context->setShadow(IntSize(styleToUse->textShadow()->x, styleToUse->textShadow()->y), + styleToUse->textShadow()->blur, styleToUse->textShadow()->color); + setShadow = true; + } + + IntPoint origin((int) svgChar.x, (int) svgChar.y); + TextRun run = svgTextRunForInlineTextBox(chars, length, styleToUse, this, svgChar.x); + +#if ENABLE(SVG_FONTS) + // SVG Fonts need access to the paint server used to draw the current text chunk. + // They need to be able to call renderPath() on a SVGPaintServer object. + run.setActivePaintServer(activePaintServer); +#endif + + paintInfo.context->drawText(run, origin); + + if (paintInfo.phase != PaintPhaseSelection) { + paintDocumentMarkers(paintInfo.context, tx, ty, styleToUse, font, false); + + if (useCustomUnderlines) { + const Vector<CompositionUnderline>& underlines = text->document()->frame()->editor()->customCompositionUnderlines(); + size_t numUnderlines = underlines.size(); + + for (size_t index = 0; index < numUnderlines; ++index) { + const CompositionUnderline& underline = underlines[index]; + + if (underline.endOffset <= start()) + // underline is completely before this run. This might be an underline that sits + // before the first run we draw, or underlines that were within runs we skipped + // due to truncation. + continue; + + if (underline.startOffset <= end()) { + // underline intersects this run. Paint it. + paintCompositionUnderline(paintInfo.context, tx, ty, underline); + if (underline.endOffset > end() + 1) + // underline also runs into the next run. Bail now, no more marker advancement. + break; + } else + // underline is completely after this run, bail. A later run will paint it. + break; + } + } + + } + + if (setShadow) + paintInfo.context->clearShadow(); + + if (!ctm.isIdentity()) + paintInfo.context->concatCTM(ctm.inverse()); +} + +void SVGInlineTextBox::paintSelection(int boxStartOffset, const SVGChar& svgChar, const UChar* chars, int length, GraphicsContext* p, RenderStyle* style, const Font* f) +{ + if (selectionState() == RenderObject::SelectionNone) + return; + + int startPos, endPos; + selectionStartEnd(startPos, endPos); + + if (startPos >= endPos) + return; + + Color textColor = style->color(); + Color color = object()->selectionBackgroundColor(); + if (!color.isValid() || color.alpha() == 0) + return; + + // If the text color ends up being the same as the selection background, invert the selection + // background. This should basically never happen, since the selection has transparency. + if (textColor == color) + color = Color(0xff - color.red(), 0xff - color.green(), 0xff - color.blue()); + + // Map from text box positions and a given start offset to chunk positions + // 'boxStartOffset' represents the beginning of the text chunk. + if ((startPos > boxStartOffset && endPos > boxStartOffset + length) || boxStartOffset >= endPos) + return; + + if (endPos > boxStartOffset + length) + endPos = boxStartOffset + length; + + if (startPos < boxStartOffset) + startPos = boxStartOffset; + + ASSERT(startPos >= boxStartOffset); + ASSERT(endPos <= boxStartOffset + length); + ASSERT(startPos < endPos); + + p->save(); + + int adjust = startPos >= boxStartOffset ? boxStartOffset : 0; + p->drawHighlightForText(svgTextRunForInlineTextBox(textObject()->text()->characters() + start() + boxStartOffset, length, style, this, svgChar.x), + IntPoint((int) svgChar.x, (int) svgChar.y - f->ascent()), + f->ascent() + f->descent(), color, startPos - adjust, endPos - adjust); + + p->restore(); +} + +static inline Path pathForDecoration(ETextDecoration decoration, RenderObject* object, float x, float y, float width) +{ + float thickness = SVGRenderStyle::cssPrimitiveToLength(object, object->style()->svgStyle()->strokeWidth(), 1.0f); + + const Font& font = object->style()->font(); + thickness = max(thickness * powf(font.size(), 2.0f) / font.unitsPerEm(), 1.0f); + + if (decoration == UNDERLINE) + y += thickness * 1.5f; // For compatibility with Batik/Opera + else if (decoration == OVERLINE) + y += thickness; + + float halfThickness = thickness / 2.0f; + return Path::createRectangle(FloatRect(x + halfThickness, y, width - 2.0f * halfThickness, thickness)); +} + +void SVGInlineTextBox::paintDecoration(ETextDecoration decoration, GraphicsContext* context, int tx, int ty, int width, const SVGChar& svgChar, const SVGTextDecorationInfo& info) +{ + if (object()->style()->visibility() != VISIBLE) + return; + + // This function does NOT accept combinated text decorations. It's meant to be invoked for just one. + ASSERT(decoration == TDNONE || decoration == UNDERLINE || decoration == OVERLINE || decoration == LINE_THROUGH || decoration == BLINK); + + bool isFilled = info.fillServerMap.contains(decoration); + bool isStroked = info.strokeServerMap.contains(decoration); + + if (!isFilled && !isStroked) + return; + + if (decoration == UNDERLINE) + ty += m_baseline; + else if (decoration == LINE_THROUGH) + ty += 2 * m_baseline / 3; + + context->save(); + context->beginPath(); + + AffineTransform ctm = svgChar.characterTransform(); + if (!ctm.isIdentity()) + context->concatCTM(ctm); + + if (isFilled) { + if (RenderObject* fillObject = info.fillServerMap.get(decoration)) { + if (SVGPaintServer* fillPaintServer = SVGPaintServer::fillPaintServer(fillObject->style(), fillObject)) { + context->addPath(pathForDecoration(decoration, fillObject, tx, ty, width)); + fillPaintServer->draw(context, fillObject, ApplyToFillTargetType); + } + } + } + + if (isStroked) { + if (RenderObject* strokeObject = info.strokeServerMap.get(decoration)) { + if (SVGPaintServer* strokePaintServer = SVGPaintServer::strokePaintServer(strokeObject->style(), strokeObject)) { + context->addPath(pathForDecoration(decoration, strokeObject, tx, ty, width)); + strokePaintServer->draw(context, strokeObject, ApplyToStrokeTargetType); + } + } + } + + context->restore(); +} + +} // namespace WebCore + +#endif |