1 /** 2 * Copyright (C) 2007 Rob Buis <buis (at) kde.org> 3 * (C) 2007 Nikolas Zimmermann <zimmermann (at) kde.org> 4 * 5 * This library is free software; you can redistribute it and/or 6 * modify it under the terms of the GNU Library General Public 7 * License as published by the Free Software Foundation; either 8 * version 2 of the License, or (at your option) any later version. 9 * 10 * This library is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 * Library General Public License for more details. 14 * 15 * You should have received a copy of the GNU Library General Public License 16 * along with this library; see the file COPYING.LIB. If not, write to 17 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 * Boston, MA 02110-1301, USA. 19 * 20 */ 21 22 #include "config.h" 23 24 #if ENABLE(SVG) 25 #include "SVGInlineTextBox.h" 26 27 #include "Document.h" 28 #include "Editor.h" 29 #include "Frame.h" 30 #include "GraphicsContext.h" 31 #include "InlineFlowBox.h" 32 #include "Range.h" 33 #include "SVGPaintServer.h" 34 #include "SVGRootInlineBox.h" 35 #include "Text.h" 36 37 #include <float.h> 38 39 namespace WebCore { 40 41 SVGInlineTextBox::SVGInlineTextBox(RenderObject* obj) 42 : InlineTextBox(obj) 43 , m_height(0) 44 { 45 } 46 47 int SVGInlineTextBox::selectionTop() 48 { 49 return m_y; 50 } 51 52 int SVGInlineTextBox::selectionHeight() 53 { 54 return m_height; 55 } 56 57 SVGRootInlineBox* SVGInlineTextBox::svgRootInlineBox() const 58 { 59 // Find associated root inline box 60 InlineFlowBox* parentBox = parent(); 61 62 while (parentBox && !parentBox->isRootInlineBox()) 63 parentBox = parentBox->parent(); 64 65 ASSERT(parentBox); 66 ASSERT(parentBox->isRootInlineBox()); 67 68 if (!parentBox->isSVGRootInlineBox()) 69 return 0; 70 71 return static_cast<SVGRootInlineBox*>(parentBox); 72 } 73 74 float SVGInlineTextBox::calculateGlyphWidth(RenderStyle* style, int offset, int extraCharsAvailable, int& charsConsumed, String& glyphName) const 75 { 76 ASSERT(style); 77 return style->font().floatWidth(svgTextRunForInlineTextBox(textRenderer()->text()->characters() + offset, 1, style, this, 0), extraCharsAvailable, charsConsumed, glyphName); 78 } 79 80 float SVGInlineTextBox::calculateGlyphHeight(RenderStyle* style, int, int) const 81 { 82 // This is just a guess, and the only purpose of this function is to centralize this hack. 83 // In real-life top-top-bottom scripts this won't be enough, I fear. 84 return style->font().ascent() + style->font().descent(); 85 } 86 87 FloatRect SVGInlineTextBox::calculateGlyphBoundaries(RenderStyle* style, int offset, const SVGChar& svgChar) const 88 { 89 const Font& font = style->font(); 90 91 // Take RTL text into account and pick right glyph width/height. 92 float glyphWidth = 0.0f; 93 94 // FIXME: account for multi-character glyphs 95 int charsConsumed; 96 String glyphName; 97 if (direction() == LTR) 98 glyphWidth = calculateGlyphWidth(style, offset, 0, charsConsumed, glyphName); 99 else 100 glyphWidth = calculateGlyphWidth(style, start() + end() - offset, 0, charsConsumed, glyphName); 101 102 float x1 = svgChar.x; 103 float x2 = svgChar.x + glyphWidth; 104 105 float y1 = svgChar.y - font.ascent(); 106 float y2 = svgChar.y + font.descent(); 107 108 FloatRect glyphRect(x1, y1, x2 - x1, y2 - y1); 109 110 // Take per-character transformations into account 111 glyphRect = svgChar.characterTransform().mapRect(glyphRect); 112 113 return glyphRect; 114 } 115 116 // Helper class for closestCharacterToPosition() 117 struct SVGInlineTextBoxClosestCharacterToPositionWalker { 118 SVGInlineTextBoxClosestCharacterToPositionWalker(int x, int y) 119 : m_character(0) 120 , m_distance(FLT_MAX) 121 , m_x(x) 122 , m_y(y) 123 , m_offsetOfHitCharacter(0) 124 { 125 } 126 127 void chunkPortionCallback(SVGInlineTextBox* textBox, int startOffset, const AffineTransform& chunkCtm, 128 const Vector<SVGChar>::iterator& start, const Vector<SVGChar>::iterator& end) 129 { 130 RenderStyle* style = textBox->textRenderer()->style(); 131 132 Vector<SVGChar>::iterator closestCharacter = 0; 133 unsigned int closestOffset = UINT_MAX; 134 135 for (Vector<SVGChar>::iterator it = start; it != end; ++it) { 136 if (it->isHidden()) 137 continue; 138 139 unsigned int newOffset = textBox->start() + (it - start) + startOffset; 140 FloatRect glyphRect = chunkCtm.mapRect(textBox->calculateGlyphBoundaries(style, newOffset, *it)); 141 142 // Take RTL text into account and pick right glyph width/height. 143 // NOTE: This offset has to be corrected _after_ calling calculateGlyphBoundaries 144 if (textBox->direction() == RTL) 145 newOffset = textBox->start() + textBox->end() - newOffset; 146 147 // Calculate distances relative to the glyph mid-point. I hope this is accurate enough. 148 float xDistance = glyphRect.x() + glyphRect.width() / 2.0f - m_x; 149 float yDistance = glyphRect.y() - glyphRect.height() / 2.0f - m_y; 150 151 float newDistance = sqrtf(xDistance * xDistance + yDistance * yDistance); 152 if (newDistance <= m_distance) { 153 m_distance = newDistance; 154 closestOffset = newOffset; 155 closestCharacter = it; 156 } 157 } 158 159 if (closestOffset != UINT_MAX) { 160 // Record current chunk, if it contains the current closest character next to the mouse. 161 m_character = closestCharacter; 162 m_offsetOfHitCharacter = closestOffset; 163 } 164 } 165 166 SVGChar* character() const 167 { 168 return m_character; 169 } 170 171 int offsetOfHitCharacter() const 172 { 173 if (!m_character) 174 return 0; 175 176 return m_offsetOfHitCharacter; 177 } 178 179 private: 180 Vector<SVGChar>::iterator m_character; 181 float m_distance; 182 183 int m_x; 184 int m_y; 185 int m_offsetOfHitCharacter; 186 }; 187 188 // Helper class for selectionRect() 189 struct SVGInlineTextBoxSelectionRectWalker { 190 SVGInlineTextBoxSelectionRectWalker() 191 { 192 } 193 194 void chunkPortionCallback(SVGInlineTextBox* textBox, int startOffset, const AffineTransform& chunkCtm, 195 const Vector<SVGChar>::iterator& start, const Vector<SVGChar>::iterator& end) 196 { 197 RenderStyle* style = textBox->textRenderer()->style(); 198 199 for (Vector<SVGChar>::iterator it = start; it != end; ++it) { 200 if (it->isHidden()) 201 continue; 202 203 unsigned int newOffset = textBox->start() + (it - start) + startOffset; 204 m_selectionRect.unite(textBox->calculateGlyphBoundaries(style, newOffset, *it)); 205 } 206 207 m_selectionRect = chunkCtm.mapRect(m_selectionRect); 208 } 209 210 FloatRect selectionRect() const 211 { 212 return m_selectionRect; 213 } 214 215 private: 216 FloatRect m_selectionRect; 217 }; 218 219 SVGChar* SVGInlineTextBox::closestCharacterToPosition(int x, int y, int& offsetOfHitCharacter) const 220 { 221 SVGRootInlineBox* rootBox = svgRootInlineBox(); 222 if (!rootBox) 223 return 0; 224 225 SVGInlineTextBoxClosestCharacterToPositionWalker walkerCallback(x, y); 226 SVGTextChunkWalker<SVGInlineTextBoxClosestCharacterToPositionWalker> walker(&walkerCallback, &SVGInlineTextBoxClosestCharacterToPositionWalker::chunkPortionCallback); 227 228 rootBox->walkTextChunks(&walker, this); 229 230 offsetOfHitCharacter = walkerCallback.offsetOfHitCharacter(); 231 return walkerCallback.character(); 232 } 233 234 bool SVGInlineTextBox::svgCharacterHitsPosition(int x, int y, int& closestOffsetInBox) const 235 { 236 int offsetOfHitCharacter = 0; 237 SVGChar* charAtPosPtr = closestCharacterToPosition(x, y, offsetOfHitCharacter); 238 if (!charAtPosPtr) 239 return false; 240 241 SVGChar& charAtPos = *charAtPosPtr; 242 RenderStyle* style = textRenderer()->style(m_firstLine); 243 FloatRect glyphRect = calculateGlyphBoundaries(style, offsetOfHitCharacter, charAtPos); 244 245 // FIXME: Why? 246 if (direction() == RTL) 247 offsetOfHitCharacter++; 248 249 // The caller actually the closest offset before/after the hit char 250 // closestCharacterToPosition returns us offsetOfHitCharacter. 251 closestOffsetInBox = offsetOfHitCharacter; 252 253 // FIXME: (bug 13910) This code does not handle bottom-to-top/top-to-bottom vertical text. 254 255 // Check whether y position hits the current character 256 if (y < charAtPos.y - glyphRect.height() || y > charAtPos.y) 257 return false; 258 259 // Check whether x position hits the current character 260 if (x < charAtPos.x) { 261 if (closestOffsetInBox > 0 && direction() == LTR) 262 return true; 263 else if (closestOffsetInBox < (int) end() && direction() == RTL) 264 return true; 265 266 return false; 267 } 268 269 // Adjust the closest offset to after the char if x was after the char midpoint 270 if (x >= charAtPos.x + glyphRect.width() / 2.0) 271 closestOffsetInBox += direction() == RTL ? -1 : 1; 272 273 // If we are past the last glyph of this box, don't mark it as 'hit' 274 if (x >= charAtPos.x + glyphRect.width() && closestOffsetInBox == (int) end()) 275 return false; 276 277 return true; 278 } 279 280 int SVGInlineTextBox::offsetForPosition(int, bool) const 281 { 282 // SVG doesn't use the offset <-> position selection system. 283 ASSERT_NOT_REACHED(); 284 return 0; 285 } 286 287 int SVGInlineTextBox::positionForOffset(int) const 288 { 289 // SVG doesn't use the offset <-> position selection system. 290 ASSERT_NOT_REACHED(); 291 return 0; 292 } 293 294 bool SVGInlineTextBox::nodeAtPoint(const HitTestRequest&, HitTestResult& result, int x, int y, int tx, int ty) 295 { 296 ASSERT(!isLineBreak()); 297 298 IntRect rect = selectionRect(0, 0, 0, len()); 299 if (renderer()->style()->visibility() == VISIBLE && rect.contains(x, y)) { 300 renderer()->updateHitTestResult(result, IntPoint(x - tx, y - ty)); 301 return true; 302 } 303 304 return false; 305 } 306 307 IntRect SVGInlineTextBox::selectionRect(int, int, int startPos, int endPos) 308 { 309 if (startPos >= endPos) 310 return IntRect(); 311 312 // TODO: Actually respect startPos/endPos - we're returning the _full_ selectionRect 313 // here. This won't lead to visible bugs, but to extra work being done. Investigate. 314 SVGRootInlineBox* rootBox = svgRootInlineBox(); 315 if (!rootBox) 316 return IntRect(); 317 318 SVGInlineTextBoxSelectionRectWalker walkerCallback; 319 SVGTextChunkWalker<SVGInlineTextBoxSelectionRectWalker> walker(&walkerCallback, &SVGInlineTextBoxSelectionRectWalker::chunkPortionCallback); 320 321 rootBox->walkTextChunks(&walker, this); 322 return enclosingIntRect(walkerCallback.selectionRect()); 323 } 324 325 bool SVGInlineTextBox::chunkSelectionStartEnd(const UChar* chunk, int chunkLength, int& selectionStart, int& selectionEnd) 326 { 327 // NOTE: We ignore SVGInlineTextBox::m_start here because it is always 0. 328 // Curently SVG doesn't use HTML block-level layout, in which m_start would be set. 329 330 int chunkStart = chunk - textRenderer()->characters(); 331 ASSERT(0 <= chunkStart); 332 333 selectionStartEnd(selectionStart, selectionEnd); 334 if (selectionEnd <= chunkStart) 335 return false; 336 if (chunkStart + chunkLength <= selectionStart) 337 return false; 338 339 // Map indices from view-global to chunk-local. 340 selectionStart -= chunkStart; 341 selectionEnd -= chunkStart; 342 // Then clamp with chunk range 343 if (selectionStart < 0) 344 selectionStart = 0; 345 if (chunkLength < selectionEnd) 346 selectionEnd = chunkLength; 347 348 return selectionStart < selectionEnd; 349 } 350 351 void SVGInlineTextBox::paintCharacters(RenderObject::PaintInfo& paintInfo, int tx, int ty, const SVGChar& svgChar, const UChar* chars, int length, SVGTextPaintInfo& textPaintInfo) 352 { 353 if (renderer()->style()->visibility() != VISIBLE || paintInfo.phase == PaintPhaseOutline) 354 return; 355 356 ASSERT(paintInfo.phase != PaintPhaseSelfOutline && paintInfo.phase != PaintPhaseChildOutlines); 357 358 RenderText* text = textRenderer(); 359 ASSERT(text); 360 361 bool isPrinting = text->document()->printing(); 362 363 // Determine whether or not we're selected. 364 bool haveSelection = !isPrinting && selectionState() != RenderObject::SelectionNone; 365 if (!haveSelection && paintInfo.phase == PaintPhaseSelection) 366 // When only painting the selection, don't bother to paint if there is none. 367 return; 368 369 // Determine whether or not we have a composition. 370 bool containsComposition = text->document()->frame()->editor()->compositionNode() == text->node(); 371 bool useCustomUnderlines = containsComposition && text->document()->frame()->editor()->compositionUsesCustomUnderlines(); 372 373 // Set our font 374 RenderStyle* styleToUse = text->style(isFirstLineStyle()); 375 const Font& font = styleToUse->font(); 376 377 AffineTransform ctm = svgChar.characterTransform(); 378 if (!ctm.isIdentity()) 379 paintInfo.context->concatCTM(ctm); 380 381 // 1. Paint backgrounds behind text if needed. Examples of such backgrounds include selection 382 // and marked text. 383 if (paintInfo.phase != PaintPhaseSelection && !isPrinting && textPaintInfo.subphase == SVGTextPaintSubphaseBackground) { 384 #if PLATFORM(MAC) 385 // Custom highlighters go behind everything else. 386 if (styleToUse->highlight() != nullAtom && !paintInfo.context->paintingDisabled()) 387 paintCustomHighlight(tx, ty, styleToUse->highlight()); 388 #endif 389 390 if (containsComposition && !useCustomUnderlines) 391 paintCompositionBackground(paintInfo.context, tx, ty, styleToUse, font, 392 text->document()->frame()->editor()->compositionStart(), 393 text->document()->frame()->editor()->compositionEnd()); 394 395 paintDocumentMarkers(paintInfo.context, tx, ty, styleToUse, font, true); 396 397 if (haveSelection && !useCustomUnderlines) { 398 int boxStartOffset = chars - text->characters() - start(); 399 paintSelection(boxStartOffset, svgChar, chars, length, paintInfo.context, styleToUse, font); 400 } 401 } 402 403 bool isGlyphPhase = textPaintInfo.subphase == SVGTextPaintSubphaseGlyphFill || textPaintInfo.subphase == SVGTextPaintSubphaseGlyphStroke; 404 bool isSelectionGlyphPhase = textPaintInfo.subphase == SVGTextPaintSubphaseGlyphFillSelection || textPaintInfo.subphase == SVGTextPaintSubphaseGlyphStrokeSelection; 405 406 if (isGlyphPhase || isSelectionGlyphPhase) { 407 // Set a text shadow if we have one. 408 // FIXME: Support multiple shadow effects. Need more from the CG API before 409 // we can do this. 410 bool setShadow = false; 411 if (styleToUse->textShadow()) { 412 paintInfo.context->setShadow(IntSize(styleToUse->textShadow()->x, styleToUse->textShadow()->y), 413 styleToUse->textShadow()->blur, styleToUse->textShadow()->color, 414 styleToUse->colorSpace()); 415 setShadow = true; 416 } 417 418 IntPoint origin((int) svgChar.x, (int) svgChar.y); 419 TextRun run = svgTextRunForInlineTextBox(chars, length, styleToUse, this, svgChar.x); 420 421 #if ENABLE(SVG_FONTS) 422 // SVG Fonts need access to the paint server used to draw the current text chunk. 423 // They need to be able to call renderPath() on a SVGPaintServer object. 424 ASSERT(textPaintInfo.activePaintServer); 425 run.setActivePaintServer(textPaintInfo.activePaintServer); 426 #endif 427 428 int selectionStart = 0; 429 int selectionEnd = 0; 430 bool haveSelectedRange = haveSelection && chunkSelectionStartEnd(chars, length, selectionStart, selectionEnd); 431 432 if (isGlyphPhase) { 433 if (haveSelectedRange) { 434 paintInfo.context->drawText(font, run, origin, 0, selectionStart); 435 paintInfo.context->drawText(font, run, origin, selectionEnd, run.length()); 436 } else 437 paintInfo.context->drawText(font, run, origin); 438 } else { 439 ASSERT(isSelectionGlyphPhase); 440 if (haveSelectedRange) 441 paintInfo.context->drawText(font, run, origin, selectionStart, selectionEnd); 442 } 443 444 if (setShadow) 445 paintInfo.context->clearShadow(); 446 } 447 448 if (paintInfo.phase != PaintPhaseSelection && textPaintInfo.subphase == SVGTextPaintSubphaseForeground) { 449 paintDocumentMarkers(paintInfo.context, tx, ty, styleToUse, font, false); 450 451 if (useCustomUnderlines) { 452 const Vector<CompositionUnderline>& underlines = text->document()->frame()->editor()->customCompositionUnderlines(); 453 size_t numUnderlines = underlines.size(); 454 455 for (size_t index = 0; index < numUnderlines; ++index) { 456 const CompositionUnderline& underline = underlines[index]; 457 458 if (underline.endOffset <= start()) 459 // underline is completely before this run. This might be an underline that sits 460 // before the first run we draw, or underlines that were within runs we skipped 461 // due to truncation. 462 continue; 463 464 if (underline.startOffset <= end()) { 465 // underline intersects this run. Paint it. 466 paintCompositionUnderline(paintInfo.context, tx, ty, underline); 467 if (underline.endOffset > end() + 1) 468 // underline also runs into the next run. Bail now, no more marker advancement. 469 break; 470 } else 471 // underline is completely after this run, bail. A later run will paint it. 472 break; 473 } 474 } 475 476 } 477 478 if (!ctm.isIdentity()) 479 paintInfo.context->concatCTM(ctm.inverse()); 480 } 481 482 void SVGInlineTextBox::paintSelection(int boxStartOffset, const SVGChar& svgChar, const UChar*, int length, GraphicsContext* p, RenderStyle* style, const Font& font) 483 { 484 if (selectionState() == RenderObject::SelectionNone) 485 return; 486 487 int startPos, endPos; 488 selectionStartEnd(startPos, endPos); 489 490 if (startPos >= endPos) 491 return; 492 493 Color textColor = style->color(); 494 Color color = renderer()->selectionBackgroundColor(); 495 if (!color.isValid() || color.alpha() == 0) 496 return; 497 498 // If the text color ends up being the same as the selection background, invert the selection 499 // background. This should basically never happen, since the selection has transparency. 500 if (textColor == color) 501 color = Color(0xff - color.red(), 0xff - color.green(), 0xff - color.blue()); 502 503 // Map from text box positions and a given start offset to chunk positions 504 // 'boxStartOffset' represents the beginning of the text chunk. 505 if ((startPos > boxStartOffset && endPos > boxStartOffset + length) || boxStartOffset >= endPos) 506 return; 507 508 if (endPos > boxStartOffset + length) 509 endPos = boxStartOffset + length; 510 511 if (startPos < boxStartOffset) 512 startPos = boxStartOffset; 513 514 ASSERT(startPos >= boxStartOffset); 515 ASSERT(endPos <= boxStartOffset + length); 516 ASSERT(startPos < endPos); 517 518 p->save(); 519 520 int adjust = startPos >= boxStartOffset ? boxStartOffset : 0; 521 p->drawHighlightForText(font, svgTextRunForInlineTextBox(textRenderer()->text()->characters() + start() + boxStartOffset, length, style, this, svgChar.x), 522 IntPoint((int) svgChar.x, (int) svgChar.y - font.ascent()), 523 font.ascent() + font.descent(), color, style->colorSpace(), startPos - adjust, endPos - adjust); 524 525 p->restore(); 526 } 527 528 static inline Path pathForDecoration(ETextDecoration decoration, RenderObject* object, float x, float y, float width) 529 { 530 float thickness = SVGRenderStyle::cssPrimitiveToLength(object, object->style()->svgStyle()->strokeWidth(), 1.0f); 531 532 const Font& font = object->style()->font(); 533 thickness = max(thickness * powf(font.size(), 2.0f) / font.unitsPerEm(), 1.0f); 534 535 if (decoration == UNDERLINE) 536 y += thickness * 1.5f; // For compatibility with Batik/Opera 537 else if (decoration == OVERLINE) 538 y += thickness; 539 540 float halfThickness = thickness / 2.0f; 541 return Path::createRectangle(FloatRect(x + halfThickness, y, width - 2.0f * halfThickness, thickness)); 542 } 543 544 void SVGInlineTextBox::paintDecoration(ETextDecoration decoration, GraphicsContext* context, int tx, int ty, int width, const SVGChar& svgChar, const SVGTextDecorationInfo& info) 545 { 546 if (renderer()->style()->visibility() != VISIBLE) 547 return; 548 549 // This function does NOT accept combinated text decorations. It's meant to be invoked for just one. 550 ASSERT(decoration == TDNONE || decoration == UNDERLINE || decoration == OVERLINE || decoration == LINE_THROUGH || decoration == BLINK); 551 552 bool isFilled = info.fillServerMap.contains(decoration); 553 bool isStroked = info.strokeServerMap.contains(decoration); 554 555 if (!isFilled && !isStroked) 556 return; 557 558 int baseline = renderer()->style(m_firstLine)->font().ascent(); 559 if (decoration == UNDERLINE) 560 ty += baseline; 561 else if (decoration == LINE_THROUGH) 562 ty += 2 * baseline / 3; 563 564 context->save(); 565 context->beginPath(); 566 567 AffineTransform ctm = svgChar.characterTransform(); 568 if (!ctm.isIdentity()) 569 context->concatCTM(ctm); 570 571 if (isFilled) { 572 if (RenderObject* fillObject = info.fillServerMap.get(decoration)) { 573 if (SVGPaintServer* fillPaintServer = SVGPaintServer::fillPaintServer(fillObject->style(), fillObject)) { 574 context->addPath(pathForDecoration(decoration, fillObject, tx, ty, width)); 575 fillPaintServer->draw(context, fillObject, ApplyToFillTargetType); 576 } 577 } 578 } 579 580 if (isStroked) { 581 if (RenderObject* strokeObject = info.strokeServerMap.get(decoration)) { 582 if (SVGPaintServer* strokePaintServer = SVGPaintServer::strokePaintServer(strokeObject->style(), strokeObject)) { 583 context->addPath(pathForDecoration(decoration, strokeObject, tx, ty, width)); 584 strokePaintServer->draw(context, strokeObject, ApplyToStrokeTargetType); 585 } 586 } 587 } 588 589 context->restore(); 590 } 591 592 } // namespace WebCore 593 594 #endif 595