1/*
2 * Copyright (C) 2011 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 *
8 * 1.  Redistributions of source code must retain the above copyright
9 *     notice, this list of conditions and the following disclaimer.
10 * 2.  Redistributions in binary form must reproduce the above copyright
11 *     notice, this list of conditions and the following disclaimer in the
12 *     documentation and/or other materials provided with the distribution.
13 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14 *     its contributors may be used to endorse or promote products derived
15 *     from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29#include "config.h"
30#include "DOMNodeHighlighter.h"
31
32#if ENABLE(INSPECTOR)
33
34#include "Element.h"
35#include "Frame.h"
36#include "FrameView.h"
37#include "GraphicsContext.h"
38#include "Page.h"
39#include "Range.h"
40#include "RenderInline.h"
41#include "Settings.h"
42#include "StyledElement.h"
43#include "TextRun.h"
44
45namespace WebCore {
46
47namespace {
48
49Path quadToPath(const FloatQuad& quad)
50{
51    Path quadPath;
52    quadPath.moveTo(quad.p1());
53    quadPath.addLineTo(quad.p2());
54    quadPath.addLineTo(quad.p3());
55    quadPath.addLineTo(quad.p4());
56    quadPath.closeSubpath();
57    return quadPath;
58}
59
60void drawOutlinedQuad(GraphicsContext& context, const FloatQuad& quad, const Color& fillColor)
61{
62    static const int outlineThickness = 2;
63    static const Color outlineColor(62, 86, 180, 228);
64
65    Path quadPath = quadToPath(quad);
66
67    // Clip out the quad, then draw with a 2px stroke to get a pixel
68    // of outline (because inflating a quad is hard)
69    {
70        context.save();
71        context.clipOut(quadPath);
72
73        context.setStrokeThickness(outlineThickness);
74        context.setStrokeColor(outlineColor, ColorSpaceDeviceRGB);
75        context.strokePath(quadPath);
76
77        context.restore();
78    }
79
80    // Now do the fill
81    context.setFillColor(fillColor, ColorSpaceDeviceRGB);
82    context.fillPath(quadPath);
83}
84
85void drawOutlinedQuadWithClip(GraphicsContext& context, const FloatQuad& quad, const FloatQuad& clipQuad, const Color& fillColor)
86{
87    context.save();
88    Path clipQuadPath = quadToPath(clipQuad);
89    context.clipOut(clipQuadPath);
90    drawOutlinedQuad(context, quad, fillColor);
91    context.restore();
92}
93
94void drawHighlightForBox(GraphicsContext& context, const FloatQuad& contentQuad, const FloatQuad& paddingQuad, const FloatQuad& borderQuad, const FloatQuad& marginQuad)
95{
96    static const Color contentBoxColor(125, 173, 217, 128);
97    static const Color paddingBoxColor(125, 173, 217, 160);
98    static const Color borderBoxColor(125, 173, 217, 192);
99    static const Color marginBoxColor(125, 173, 217, 228);
100
101    if (marginQuad != borderQuad)
102        drawOutlinedQuadWithClip(context, marginQuad, borderQuad, marginBoxColor);
103    if (borderQuad != paddingQuad)
104        drawOutlinedQuadWithClip(context, borderQuad, paddingQuad, borderBoxColor);
105    if (paddingQuad != contentQuad)
106        drawOutlinedQuadWithClip(context, paddingQuad, contentQuad, paddingBoxColor);
107
108    drawOutlinedQuad(context, contentQuad, contentBoxColor);
109}
110
111void drawHighlightForLineBoxesOrSVGRenderer(GraphicsContext& context, const Vector<FloatQuad>& lineBoxQuads)
112{
113    static const Color lineBoxColor(125, 173, 217, 128);
114
115    for (size_t i = 0; i < lineBoxQuads.size(); ++i)
116        drawOutlinedQuad(context, lineBoxQuads[i], lineBoxColor);
117}
118
119inline IntSize frameToMainFrameOffset(Frame* frame)
120{
121    IntPoint mainFramePoint = frame->page()->mainFrame()->view()->windowToContents(frame->view()->contentsToWindow(IntPoint()));
122    return mainFramePoint - IntPoint();
123}
124
125void drawElementTitle(GraphicsContext& context, Node* node, const IntRect& boundingBox, const IntRect& anchorBox, const FloatRect& overlayRect, WebCore::Settings* settings)
126{
127    static const int rectInflatePx = 4;
128    static const int fontHeightPx = 12;
129    static const int borderWidthPx = 1;
130    static const Color tooltipBackgroundColor(255, 255, 194, 255);
131    static const Color tooltipBorderColor(Color::black);
132    static const Color tooltipFontColor(Color::black);
133
134    Element* element = static_cast<Element*>(node);
135    bool isXHTML = element->document()->isXHTMLDocument();
136    String nodeTitle = isXHTML ? element->nodeName() : element->nodeName().lower();
137    const AtomicString& idValue = element->getIdAttribute();
138    if (!idValue.isNull() && !idValue.isEmpty()) {
139        nodeTitle += "#";
140        nodeTitle += idValue;
141    }
142    if (element->hasClass() && element->isStyledElement()) {
143        const SpaceSplitString& classNamesString = static_cast<StyledElement*>(element)->classNames();
144        size_t classNameCount = classNamesString.size();
145        if (classNameCount) {
146            HashSet<AtomicString> usedClassNames;
147            for (size_t i = 0; i < classNameCount; ++i) {
148                const AtomicString& className = classNamesString[i];
149                if (usedClassNames.contains(className))
150                    continue;
151                usedClassNames.add(className);
152                nodeTitle += ".";
153                nodeTitle += className;
154            }
155        }
156    }
157
158    nodeTitle += " [";
159    nodeTitle += String::number(boundingBox.width());
160    nodeTitle.append(static_cast<UChar>(0x00D7)); // &times;
161    nodeTitle += String::number(boundingBox.height());
162    nodeTitle += "]";
163
164    FontDescription desc;
165    FontFamily family;
166    family.setFamily(settings->fixedFontFamily());
167    desc.setFamily(family);
168    desc.setComputedSize(fontHeightPx);
169    Font font = Font(desc, 0, 0);
170    font.update(0);
171
172    TextRun nodeTitleRun(nodeTitle);
173    IntPoint titleBasePoint = IntPoint(anchorBox.x(), anchorBox.maxY() - 1);
174    titleBasePoint.move(rectInflatePx, rectInflatePx);
175    IntRect titleRect = enclosingIntRect(font.selectionRectForText(nodeTitleRun, titleBasePoint, fontHeightPx));
176    titleRect.inflate(rectInflatePx);
177
178    // The initial offsets needed to compensate for a 1px-thick border stroke (which is not a part of the rectangle).
179    int dx = -borderWidthPx;
180    int dy = borderWidthPx;
181
182    // If the tip sticks beyond the right of overlayRect, right-align the tip with the said boundary.
183    if (titleRect.maxX() > overlayRect.maxX())
184        dx = overlayRect.maxX() - titleRect.maxX();
185
186    // If the tip sticks beyond the left of overlayRect, left-align the tip with the said boundary.
187    if (titleRect.x() + dx < overlayRect.x())
188        dx = overlayRect.x() - titleRect.x() - borderWidthPx;
189
190    // If the tip sticks beyond the bottom of overlayRect, show the tip at top of bounding box.
191    if (titleRect.maxY() > overlayRect.maxY()) {
192        dy = anchorBox.y() - titleRect.maxY() - borderWidthPx;
193        // If the tip still sticks beyond the bottom of overlayRect, bottom-align the tip with the said boundary.
194        if (titleRect.maxY() + dy > overlayRect.maxY())
195            dy = overlayRect.maxY() - titleRect.maxY();
196    }
197
198    // If the tip sticks beyond the top of overlayRect, show the tip at top of overlayRect.
199    if (titleRect.y() + dy < overlayRect.y())
200        dy = overlayRect.y() - titleRect.y() + borderWidthPx;
201
202    titleRect.move(dx, dy);
203    context.setStrokeColor(tooltipBorderColor, ColorSpaceDeviceRGB);
204    context.setStrokeThickness(borderWidthPx);
205    context.setFillColor(tooltipBackgroundColor, ColorSpaceDeviceRGB);
206    context.drawRect(titleRect);
207    context.setFillColor(tooltipFontColor, ColorSpaceDeviceRGB);
208    context.drawText(font, nodeTitleRun, IntPoint(titleRect.x() + rectInflatePx, titleRect.y() + font.fontMetrics().height()));
209}
210
211} // anonymous namespace
212
213namespace DOMNodeHighlighter {
214
215void DrawNodeHighlight(GraphicsContext& context, Node* node)
216{
217    RenderObject* renderer = node->renderer();
218    Frame* containingFrame = node->document()->frame();
219
220    if (!renderer || !containingFrame)
221        return;
222
223    IntSize mainFrameOffset = frameToMainFrameOffset(containingFrame);
224    IntRect boundingBox = renderer->absoluteBoundingBoxRect(true);
225
226    boundingBox.move(mainFrameOffset);
227
228    IntRect titleAnchorBox = boundingBox;
229
230    FrameView* view = containingFrame->page()->mainFrame()->view();
231    FloatRect overlayRect = view->visibleContentRect();
232    if (!overlayRect.contains(boundingBox) && !boundingBox.contains(enclosingIntRect(overlayRect)))
233        overlayRect = view->visibleContentRect();
234    context.translate(-overlayRect.x(), -overlayRect.y());
235
236    // RenderSVGRoot should be highlighted through the isBox() code path, all other SVG elements should just dump their absoluteQuads().
237#if ENABLE(SVG)
238    bool isSVGRenderer = renderer->node() && renderer->node()->isSVGElement() && !renderer->isSVGRoot();
239#else
240    bool isSVGRenderer = false;
241#endif
242
243    if (renderer->isBox() && !isSVGRenderer) {
244        RenderBox* renderBox = toRenderBox(renderer);
245
246        // RenderBox returns the "pure" content area box, exclusive of the scrollbars (if present), which also count towards the content area in CSS.
247        IntRect contentBox = renderBox->contentBoxRect();
248        contentBox.setWidth(contentBox.width() + renderBox->verticalScrollbarWidth());
249        contentBox.setHeight(contentBox.height() + renderBox->horizontalScrollbarHeight());
250
251        IntRect paddingBox(contentBox.x() - renderBox->paddingLeft(), contentBox.y() - renderBox->paddingTop(),
252                           contentBox.width() + renderBox->paddingLeft() + renderBox->paddingRight(), contentBox.height() + renderBox->paddingTop() + renderBox->paddingBottom());
253        IntRect borderBox(paddingBox.x() - renderBox->borderLeft(), paddingBox.y() - renderBox->borderTop(),
254                          paddingBox.width() + renderBox->borderLeft() + renderBox->borderRight(), paddingBox.height() + renderBox->borderTop() + renderBox->borderBottom());
255        IntRect marginBox(borderBox.x() - renderBox->marginLeft(), borderBox.y() - renderBox->marginTop(),
256                          borderBox.width() + renderBox->marginLeft() + renderBox->marginRight(), borderBox.height() + renderBox->marginTop() + renderBox->marginBottom());
257
258
259        FloatQuad absContentQuad = renderBox->localToAbsoluteQuad(FloatRect(contentBox));
260        FloatQuad absPaddingQuad = renderBox->localToAbsoluteQuad(FloatRect(paddingBox));
261        FloatQuad absBorderQuad = renderBox->localToAbsoluteQuad(FloatRect(borderBox));
262        FloatQuad absMarginQuad = renderBox->localToAbsoluteQuad(FloatRect(marginBox));
263
264        absContentQuad.move(mainFrameOffset);
265        absPaddingQuad.move(mainFrameOffset);
266        absBorderQuad.move(mainFrameOffset);
267        absMarginQuad.move(mainFrameOffset);
268
269        titleAnchorBox = absMarginQuad.enclosingBoundingBox();
270
271        drawHighlightForBox(context, absContentQuad, absPaddingQuad, absBorderQuad, absMarginQuad);
272    } else if (renderer->isRenderInline() || isSVGRenderer) {
273        // FIXME: We should show margins/padding/border for inlines.
274        Vector<FloatQuad> lineBoxQuads;
275        renderer->absoluteQuads(lineBoxQuads);
276        for (unsigned i = 0; i < lineBoxQuads.size(); ++i)
277            lineBoxQuads[i] += mainFrameOffset;
278
279        drawHighlightForLineBoxesOrSVGRenderer(context, lineBoxQuads);
280    }
281
282    // Draw node title if necessary.
283
284    if (!node->isElementNode())
285        return;
286
287    WebCore::Settings* settings = containingFrame->settings();
288    drawElementTitle(context, node, boundingBox, titleAnchorBox, overlayRect, settings);
289}
290
291} // namespace DOMNodeHighlighter
292
293} // namespace WebCore
294
295#endif // ENABLE(INSPECTOR)
296