1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "content/shell/renderer/test_runner/mock_web_theme_engine.h"
6
7#include "base/logging.h"
8#include "skia/ext/platform_canvas.h"
9#include "third_party/WebKit/public/platform/WebRect.h"
10#include "third_party/WebKit/public/platform/WebSize.h"
11#include "third_party/skia/include/core/SkRect.h"
12
13using blink::WebCanvas;
14using blink::WebColor;
15using blink::WebRect;
16using blink::WebThemeEngine;
17
18namespace content {
19
20namespace {
21
22const SkColor edgeColor = SK_ColorBLACK;
23const SkColor readOnlyColor = SkColorSetRGB(0xe9, 0xc2, 0xa6);
24
25}  // namespace
26
27SkColor bgColors(WebThemeEngine::State state) {
28  switch (state) {
29    case WebThemeEngine::StateDisabled:
30      return SkColorSetRGB(0xc9, 0xc9, 0xc9);
31    case WebThemeEngine::StateHover:
32      return SkColorSetRGB(0x43, 0xf9, 0xff);
33    case WebThemeEngine::StateNormal:
34      return SkColorSetRGB(0x89, 0xc4, 0xff);
35    case WebThemeEngine::StatePressed:
36      return SkColorSetRGB(0xa9, 0xff, 0x12);
37    case WebThemeEngine::StateFocused:
38      return SkColorSetRGB(0x00, 0xf3, 0xac);
39    case WebThemeEngine::StateReadonly:
40      return SkColorSetRGB(0xf3, 0xe0, 0xd0);
41    default:
42      NOTREACHED();
43  }
44  return SkColorSetRGB(0x00, 0x00, 0xff);
45}
46
47blink::WebSize MockWebThemeEngine::getSize(WebThemeEngine::Part part) {
48    // FIXME: We use this constant to indicate we are being asked for the size of
49    // a part that we don't expect to be asked about. We return a garbage value
50    // rather than just asserting because this code doesn't have access to either
51    // WTF or base to raise an assertion or do any logging :(.
52    const blink::WebSize invalidPartSize = blink::WebSize(100, 100);
53
54    switch (part) {
55    case WebThemeEngine::PartScrollbarLeftArrow:
56        return blink::WebSize(17, 15);
57    case WebThemeEngine::PartScrollbarRightArrow:
58        return invalidPartSize;
59    case WebThemeEngine::PartScrollbarUpArrow:
60        return blink::WebSize(15, 17);
61    case WebThemeEngine::PartScrollbarDownArrow:
62        return invalidPartSize;
63    case WebThemeEngine::PartScrollbarHorizontalThumb:
64        return blink::WebSize(15, 15);
65    case WebThemeEngine::PartScrollbarVerticalThumb:
66        return blink::WebSize(15, 15);
67    case WebThemeEngine::PartScrollbarHorizontalTrack:
68        return blink::WebSize(0, 15);
69    case WebThemeEngine::PartScrollbarVerticalTrack:
70        return blink::WebSize(15, 0);
71    case WebThemeEngine::PartCheckbox:
72    case WebThemeEngine::PartRadio:
73        return blink::WebSize(13, 13);
74    case WebThemeEngine::PartSliderThumb:
75        return blink::WebSize(11, 21);
76    case WebThemeEngine::PartInnerSpinButton:
77        return blink::WebSize(15, 8);
78    default:
79        return invalidPartSize;
80    }
81}
82
83static SkIRect webRectToSkIRect(const WebRect& webRect) {
84    SkIRect irect;
85    irect.set(webRect.x, webRect.y,
86        webRect.x + webRect.width - 1, webRect.y + webRect.height - 1);
87    return irect;
88}
89
90static SkIRect validate(const SkIRect& rect, WebThemeEngine::Part part) {
91    switch (part) {
92    case WebThemeEngine::PartCheckbox:
93    case WebThemeEngine::PartRadio: {
94        SkIRect retval = rect;
95
96        // The maximum width and height is 13.
97        // Center the square in the passed rectangle.
98        const int maxControlSize = 13;
99        int controlSize = std::min(rect.width(), rect.height());
100        controlSize = std::min(controlSize, maxControlSize);
101
102        retval.fLeft   = rect.fLeft + (rect.width() / 2) - (controlSize / 2);
103        retval.fRight  = retval.fLeft + controlSize - 1;
104        retval.fTop    = rect.fTop + (rect.height() / 2) - (controlSize / 2);
105        retval.fBottom = retval.fTop + controlSize - 1;
106
107        return retval;
108    }
109    default:
110        return rect;
111    }
112}
113
114void box(SkCanvas* canvas, const SkIRect& rect, SkColor fillColor) {
115    SkPaint paint;
116
117    paint.setStyle(SkPaint::kFill_Style);
118    paint.setColor(fillColor);
119    canvas->drawIRect(rect, paint);
120
121    paint.setColor(edgeColor);
122    paint.setStyle(SkPaint::kStroke_Style);
123    canvas->drawIRect(rect, paint);
124}
125
126void line(SkCanvas* canvas, int x0, int y0, int x1, int y1, SkColor color) {
127    SkPaint paint;
128    paint.setColor(color);
129    canvas->drawLine(SkIntToScalar(x0),
130                     SkIntToScalar(y0),
131                     SkIntToScalar(x1),
132                     SkIntToScalar(y1),
133                     paint);
134}
135
136void triangle(SkCanvas* canvas,
137              int x0,
138              int y0,
139              int x1,
140              int y1,
141              int x2,
142              int y2,
143              SkColor color) {
144    SkPath path;
145    SkPaint paint;
146
147    paint.setColor(color);
148    paint.setStyle(SkPaint::kFill_Style);
149    path.incReserve(4);
150    path.moveTo(SkIntToScalar(x0), SkIntToScalar(y0));
151    path.lineTo(SkIntToScalar(x1), SkIntToScalar(y1));
152    path.lineTo(SkIntToScalar(x2), SkIntToScalar(y2));
153    path.close();
154    canvas->drawPath(path, paint);
155
156    paint.setColor(edgeColor);
157    paint.setStyle(SkPaint::kStroke_Style);
158    canvas->drawPath(path, paint);
159}
160
161void roundRect(SkCanvas* canvas, SkIRect irect, SkColor color) {
162    SkRect rect;
163    SkScalar radius = SkIntToScalar(5);
164    SkPaint paint;
165
166    rect.set(irect);
167    paint.setColor(color);
168    paint.setStyle(SkPaint::kFill_Style);
169    canvas->drawRoundRect(rect, radius, radius, paint);
170
171    paint.setColor(edgeColor);
172    paint.setStyle(SkPaint::kStroke_Style);
173    canvas->drawRoundRect(rect, radius, radius, paint);
174}
175
176void oval(SkCanvas* canvas, SkIRect irect, SkColor color) {
177    SkRect rect;
178    SkPaint paint;
179
180    rect.set(irect);
181    paint.setColor(color);
182    paint.setStyle(SkPaint::kFill_Style);
183    canvas->drawOval(rect, paint);
184
185    paint.setColor(edgeColor);
186    paint.setStyle(SkPaint::kStroke_Style);
187    canvas->drawOval(rect, paint);
188}
189
190void circle(SkCanvas* canvas, SkIRect irect, SkScalar radius, SkColor color) {
191    int left = irect.fLeft;
192    int width = irect.width();
193    int height = irect.height();
194    int top = irect.fTop;
195
196    SkScalar cy = SkIntToScalar(top  + height / 2);
197    SkScalar cx = SkIntToScalar(left + width / 2);
198    SkPaint paint;
199
200    paint.setColor(color);
201    paint.setStyle(SkPaint::kFill_Style);
202    canvas->drawCircle(cx, cy, radius, paint);
203
204    paint.setColor(edgeColor);
205    paint.setStyle(SkPaint::kStroke_Style);
206    canvas->drawCircle(cx, cy, radius, paint);
207}
208
209void nestedBoxes(SkCanvas* canvas,
210                 SkIRect irect,
211                 int indentLeft,
212                 int indentTop,
213                 int indentRight,
214                 int indentBottom,
215                 SkColor outerColor,
216                 SkColor innerColor) {
217    SkIRect lirect;
218    box(canvas, irect, outerColor);
219    lirect.set(irect.fLeft + indentLeft,
220        irect.fTop + indentTop,
221        irect.fRight - indentRight,
222        irect.fBottom - indentBottom);
223    box(canvas, lirect, innerColor);
224}
225
226void insetBox(SkCanvas* canvas,
227              SkIRect irect,
228              int indentLeft,
229              int indentTop,
230              int indentRight,
231              int indentBottom,
232              SkColor color) {
233  SkIRect lirect;
234  lirect.set(irect.fLeft + indentLeft,
235             irect.fTop + indentTop,
236             irect.fRight - indentRight,
237             irect.fBottom - indentBottom);
238  box(canvas, lirect, color);
239}
240
241void markState(SkCanvas* canvas, SkIRect irect, WebThemeEngine::State state) {
242    int left = irect.fLeft;
243    int right = irect.fRight;
244    int top = irect.fTop;
245    int bottom = irect.fBottom;
246
247    // The length of a triangle side for the corner marks.
248    const int triangleSize = 5;
249
250    switch (state) {
251    case WebThemeEngine::StateDisabled:
252    case WebThemeEngine::StateNormal:
253        // Don't visually mark these states (color is enough).
254        break;
255
256    case WebThemeEngine::StateReadonly: {
257        // The horizontal lines in a read only control are spaced by this amount.
258        const int readOnlyLineOffset = 5;
259
260        // Drawing lines across the control.
261        for (int i = top + readOnlyLineOffset; i < bottom; i += readOnlyLineOffset)
262            line(canvas, left + 1, i, right - 1, i, readOnlyColor);
263        break;
264    }
265    case WebThemeEngine::StateHover:
266        // Draw a triangle in the upper left corner of the control. (Win's "hot")
267        triangle(canvas,
268            left,                 top,
269            left + triangleSize,  top,
270            left,                 top + triangleSize,
271            edgeColor);
272        break;
273
274    case WebThemeEngine::StateFocused:
275        // Draw a triangle in the bottom right corner of the control.
276        triangle(canvas,
277            right,                bottom,
278            right - triangleSize, bottom,
279            right,                bottom - triangleSize,
280            edgeColor);
281        break;
282
283    case WebThemeEngine::StatePressed:
284        // Draw a triangle in the bottom left corner of the control.
285        triangle(canvas,
286            left,                 bottom,
287            left,                 bottom - triangleSize,
288            left + triangleSize,  bottom,
289            edgeColor);
290        break;
291
292    default:
293        // FIXME: Should we do something here to indicate that we got an invalid state?
294        // Unfortunately, we can't assert because we don't have access to WTF or base.
295        break;
296    }
297}
298
299void MockWebThemeEngine::paint(blink::WebCanvas* canvas,
300                               WebThemeEngine::Part part,
301                               WebThemeEngine::State state,
302                               const blink::WebRect& rect,
303                               const WebThemeEngine::ExtraParams* extraParams) {
304    SkIRect irect = webRectToSkIRect(rect);
305    SkPaint paint;
306
307    // Indent amounts for the check in a checkbox or radio button.
308    const int checkIndent = 3;
309
310    // Indent amounts for short and long sides of the scrollbar notches.
311    const int notchLongOffset = 1;
312    const int notchShortOffset = 4;
313    const int noOffset = 0;
314
315    // Indent amounts for the short and long sides of a scroll thumb box.
316    const int thumbLongIndent = 0;
317    const int thumbShortIndent = 2;
318
319    // Indents for the crosshatch on a scroll grip.
320    const int gripLongIndent = 3;
321    const int gripShortIndent = 5;
322
323    // Indents for the the slider track.
324    const int sliderIndent = 2;
325
326    int halfHeight = irect.height() / 2;
327    int halfWidth = irect.width() / 2;
328    int quarterHeight = irect.height() / 4;
329    int quarterWidth = irect.width() / 4;
330    int left = irect.fLeft;
331    int right = irect.fRight;
332    int top = irect.fTop;
333    int bottom = irect.fBottom;
334
335    switch (part) {
336    case WebThemeEngine::PartScrollbarDownArrow:
337        box(canvas, irect, bgColors(state));
338        triangle(canvas,
339            left  + quarterWidth, top    + quarterHeight,
340            right - quarterWidth, top    + quarterHeight,
341            left  + halfWidth,    bottom - quarterHeight,
342            edgeColor);
343        markState(canvas, irect, state);
344        break;
345
346    case WebThemeEngine::PartScrollbarLeftArrow:
347        box(canvas, irect, bgColors(state));
348        triangle(canvas,
349            right - quarterWidth, top    + quarterHeight,
350            right - quarterWidth, bottom - quarterHeight,
351            left  + quarterWidth, top    + halfHeight,
352            edgeColor);
353        break;
354
355    case WebThemeEngine::PartScrollbarRightArrow:
356        box(canvas, irect, bgColors(state));
357        triangle(canvas,
358            left  + quarterWidth, top    + quarterHeight,
359            right - quarterWidth, top    + halfHeight,
360            left  + quarterWidth, bottom - quarterHeight,
361            edgeColor);
362        break;
363
364    case WebThemeEngine::PartScrollbarUpArrow:
365        box(canvas, irect, bgColors(state));
366        triangle(canvas,
367            left  + quarterWidth, bottom - quarterHeight,
368            left  + halfWidth,    top    + quarterHeight,
369            right - quarterWidth, bottom - quarterHeight,
370            edgeColor);
371        markState(canvas, irect, state);
372        break;
373
374    case WebThemeEngine::PartScrollbarHorizontalThumb: {
375        // Draw a narrower box on top of the outside box.
376        nestedBoxes(canvas, irect, thumbLongIndent, thumbShortIndent,
377            thumbLongIndent, thumbShortIndent,
378            bgColors(state), bgColors(state));
379        // Draw a horizontal crosshatch for the grip.
380        int longOffset = halfWidth - gripLongIndent;
381        line(canvas,
382            left  + gripLongIndent, top    + halfHeight,
383            right - gripLongIndent, top    + halfHeight,
384            edgeColor);
385        line(canvas,
386            left  + longOffset,     top    + gripShortIndent,
387            left  + longOffset,     bottom - gripShortIndent,
388            edgeColor);
389        line(canvas,
390            right - longOffset,     top    + gripShortIndent,
391            right - longOffset,     bottom - gripShortIndent,
392            edgeColor);
393        markState(canvas, irect, state);
394        break;
395    }
396
397    case WebThemeEngine::PartScrollbarVerticalThumb: {
398        // Draw a shorter box on top of the outside box.
399        nestedBoxes(canvas, irect, thumbShortIndent, thumbLongIndent,
400            thumbShortIndent, thumbLongIndent,
401            bgColors(state), bgColors(state));
402        // Draw a vertical crosshatch for the grip.
403        int longOffset = halfHeight - gripLongIndent;
404        line(canvas,
405            left  + halfWidth,       top    + gripLongIndent,
406            left  + halfWidth,       bottom - gripLongIndent,
407            edgeColor);
408        line(canvas,
409            left  + gripShortIndent, top    + longOffset,
410            right - gripShortIndent, top    + longOffset,
411            edgeColor);
412        line(canvas,
413            left  + gripShortIndent, bottom - longOffset,
414            right - gripShortIndent, bottom - longOffset,
415            edgeColor);
416        markState(canvas, irect, state);
417        break;
418    }
419
420    case WebThemeEngine::PartScrollbarHorizontalTrack: {
421        int longOffset = halfHeight - notchLongOffset;
422        int shortOffset = irect.width() - notchShortOffset;
423        box(canvas, irect, bgColors(state));
424        // back, notch on right
425        insetBox(canvas,
426                 irect,
427                 noOffset,
428                 longOffset,
429                 shortOffset,
430                 longOffset,
431                 edgeColor);
432        // forward, notch on right
433        insetBox(canvas,
434                 irect,
435                 shortOffset,
436                 longOffset,
437                 noOffset,
438                 longOffset,
439                 edgeColor);
440        markState(canvas, irect, state);
441        break;
442    }
443
444    case WebThemeEngine::PartScrollbarVerticalTrack: {
445        int longOffset = halfWidth - notchLongOffset;
446        int shortOffset = irect.height() - notchShortOffset;
447        box(canvas, irect, bgColors(state));
448        // back, notch at top
449        insetBox(canvas,
450                 irect,
451                 longOffset,
452                 noOffset,
453                 longOffset,
454                 shortOffset,
455                 edgeColor);
456        // forward, notch at bottom
457        insetBox(canvas,
458                 irect,
459                 longOffset,
460                 shortOffset,
461                 longOffset,
462                 noOffset,
463                 edgeColor);
464        markState(canvas, irect, state);
465        break;
466    }
467
468    case WebThemeEngine::PartScrollbarCorner: {
469        SkIRect cornerRect = {rect.x, rect.y, rect.x + rect.width, rect.y + rect.height};
470        paint.setColor(SK_ColorWHITE);
471        paint.setStyle(SkPaint::kFill_Style);
472        paint.setXfermodeMode(SkXfermode::kSrc_Mode);
473        paint.setAntiAlias(true);
474        canvas->drawIRect(cornerRect, paint);
475        break;
476    }
477
478    case WebThemeEngine::PartCheckbox:
479        if (extraParams->button.indeterminate) {
480            nestedBoxes(canvas, irect,
481                checkIndent, halfHeight,
482                checkIndent, halfHeight,
483                bgColors(state), edgeColor);
484        } else if (extraParams->button.checked) {
485            irect = validate(irect, part);
486            nestedBoxes(canvas, irect,
487                checkIndent, checkIndent,
488                checkIndent, checkIndent,
489                bgColors(state), edgeColor);
490        } else {
491            irect = validate(irect, part);
492            box(canvas, irect, bgColors(state));
493        }
494        break;
495
496    case WebThemeEngine::PartRadio:
497        irect = validate(irect, part);
498        halfHeight = irect.height() / 2;
499        if (extraParams->button.checked) {
500            circle(canvas, irect, SkIntToScalar(halfHeight), bgColors(state));
501            circle(canvas, irect, SkIntToScalar(halfHeight - checkIndent), edgeColor);
502        } else {
503            circle(canvas, irect, SkIntToScalar(halfHeight), bgColors(state));
504        }
505        break;
506
507    case WebThemeEngine::PartButton:
508        roundRect(canvas, irect, bgColors(state));
509        markState(canvas, irect, state);
510        break;
511
512    case WebThemeEngine::PartTextField:
513        paint.setColor(extraParams->textField.backgroundColor);
514        paint.setStyle(SkPaint::kFill_Style);
515        canvas->drawIRect(irect, paint);
516
517        paint.setColor(edgeColor);
518        paint.setStyle(SkPaint::kStroke_Style);
519        canvas->drawIRect(irect, paint);
520
521        markState(canvas, irect, state);
522        break;
523
524    case WebThemeEngine::PartMenuList:
525        if (extraParams->menuList.fillContentArea) {
526            box(canvas, irect, extraParams->menuList.backgroundColor);
527        } else {
528            SkPaint paint;
529            paint.setColor(edgeColor);
530            paint.setStyle(SkPaint::kStroke_Style);
531            canvas->drawIRect(irect, paint);
532        }
533
534        // clip the drop-down arrow to be inside the select box
535        if (extraParams->menuList.arrowX - 4 > irect.fLeft)
536            irect.fLeft = extraParams->menuList.arrowX - 4;
537        if (extraParams->menuList.arrowX + 12 < irect.fRight)
538            irect.fRight = extraParams->menuList.arrowX + 12;
539
540        irect.fTop = extraParams->menuList.arrowY - (extraParams->menuList.arrowHeight) / 2;
541        irect.fBottom = extraParams->menuList.arrowY + (extraParams->menuList.arrowHeight - 1) / 2;
542        halfWidth = irect.width() / 2;
543        quarterWidth = irect.width() / 4;
544
545        if (state == WebThemeEngine::StateFocused) // FIXME: draw differenty?
546            state = WebThemeEngine::StateNormal;
547        box(canvas, irect, bgColors(state));
548        triangle(canvas,
549            irect.fLeft  + quarterWidth, irect.fTop,
550            irect.fRight - quarterWidth, irect.fTop,
551            irect.fLeft  + halfWidth,    irect.fBottom,
552            edgeColor);
553
554        break;
555
556    case WebThemeEngine::PartSliderTrack: {
557        SkIRect lirect =  irect;
558
559        // Draw a narrow rect for the track plus box hatches on the ends.
560        if (state == WebThemeEngine::StateFocused) // FIXME: draw differently?
561            state = WebThemeEngine::StateNormal;
562        if (extraParams->slider.vertical) {
563            lirect.inset(halfWidth - sliderIndent, noOffset);
564            box(canvas, lirect, bgColors(state));
565            line(canvas, left, top, right, top, edgeColor);
566            line(canvas, left, bottom, right, bottom, edgeColor);
567        } else {
568            lirect.inset(noOffset, halfHeight - sliderIndent);
569            box(canvas, lirect, bgColors(state));
570            line(canvas, left, top, left, bottom, edgeColor);
571            line(canvas, right, top, right, bottom, edgeColor);
572        }
573        break;
574    }
575
576    case WebThemeEngine::PartSliderThumb:
577        if (state == WebThemeEngine::StateFocused) // FIXME: draw differently?
578            state = WebThemeEngine::StateNormal;
579        oval(canvas, irect, bgColors(state));
580        break;
581
582    case WebThemeEngine::PartInnerSpinButton: {
583        // stack half-height up and down arrows on top of each other
584        SkIRect lirect;
585        int halfHeight = rect.height / 2;
586        if (extraParams->innerSpin.readOnly)
587            state = blink::WebThemeEngine::StateDisabled;
588
589        lirect.set(rect.x, rect.y, rect.x + rect.width - 1, rect.y + halfHeight - 1);
590        box(canvas, lirect, bgColors(state));
591        bottom = lirect.fBottom;
592        quarterHeight = lirect.height() / 4;
593        triangle(canvas,
594            left  + quarterWidth, bottom - quarterHeight,
595            right - quarterWidth, bottom - quarterHeight,
596            left  + halfWidth,    top    + quarterHeight,
597            edgeColor);
598
599        lirect.set(rect.x, rect.y + halfHeight, rect.x + rect.width - 1,
600            rect.y + 2 * halfHeight - 1);
601        top = lirect.fTop;
602        bottom = lirect.fBottom;
603        quarterHeight = lirect.height() / 4;
604        box(canvas, lirect, bgColors(state));
605        triangle(canvas,
606            left  + quarterWidth, top    + quarterHeight,
607            right - quarterWidth, top    + quarterHeight,
608            left  + halfWidth,    bottom - quarterHeight,
609            edgeColor);
610        markState(canvas, irect, state);
611        break;
612    }
613    case WebThemeEngine::PartProgressBar: {
614        paint.setColor(bgColors(state));
615        paint.setStyle(SkPaint::kFill_Style);
616        canvas->drawIRect(irect, paint);
617
618        // Emulate clipping
619        SkIRect tofill = irect;
620        if (extraParams->progressBar.determinate) {
621            tofill.set(extraParams->progressBar.valueRectX,
622                extraParams->progressBar.valueRectY,
623                extraParams->progressBar.valueRectX +
624                extraParams->progressBar.valueRectWidth - 1,
625                extraParams->progressBar.valueRectY +
626                extraParams->progressBar.valueRectHeight);
627        }
628
629        tofill.intersect(irect, tofill);
630        paint.setColor(edgeColor);
631        paint.setStyle(SkPaint::kFill_Style);
632        canvas->drawIRect(tofill, paint);
633
634        markState(canvas, irect, state);
635        break;
636    }
637    default:
638        // FIXME: Should we do something here to indicate that we got an invalid part?
639        // Unfortunately, we can't assert because we don't have access to WTF or base.
640        break;
641    }
642}
643
644}  // namespace content
645