Bug 1340661 - Manually draw checkbox and radio frames on Android. r?tn,snorp draft
authorMike Conley <mconley@mozilla.com>
Fri, 03 Mar 2017 18:36:12 -0500
changeset 494807 d991ce6f70e2ef43a63d8cd05fe46433278b5cfc
parent 493537 eb23648534779c110f3a1f2baae1849ae4a9c570
child 548210 d5b0a89c1bcc2dd29938aebc5f5f13020967c67e
push id48144
push usermconley@mozilla.com
push dateTue, 07 Mar 2017 21:15:21 +0000
reviewerstn, snorp
bugs1340661
milestone54.0a1
Bug 1340661 - Manually draw checkbox and radio frames on Android. r?tn,snorp MozReview-Commit-ID: 8IiaRZNJs16
layout/forms/nsGfxCheckboxControlFrame.cpp
layout/forms/nsGfxCheckboxControlFrame.h
layout/forms/nsGfxRadioControlFrame.cpp
layout/forms/nsGfxRadioControlFrame.h
layout/painting/nsDisplayItemTypesList.h
mobile/android/themes/core/content.css
widget/android/AndroidColors.h
widget/android/moz.build
--- a/layout/forms/nsGfxCheckboxControlFrame.cpp
+++ b/layout/forms/nsGfxCheckboxControlFrame.cpp
@@ -42,8 +42,138 @@ nsGfxCheckboxControlFrame::~nsGfxCheckbo
 
 #ifdef ACCESSIBILITY
 a11y::AccType
 nsGfxCheckboxControlFrame::AccessibleType()
 {
   return a11y::eHTMLCheckboxType;
 }
 #endif
+
+#ifdef ANDROID
+
+#include "mozilla/widget/AndroidColors.h"
+
+static void
+PaintCheckboxBorder(nsIFrame* aFrame,
+               DrawTarget* aDrawTarget,
+               const nsRect& aDirtyRect,
+               nsPoint aPt)
+{
+  nsRect rect(aPt, aFrame->GetSize());
+  rect.Deflate(aFrame->GetUsedBorderAndPadding());
+
+  // Checkbox controls aren't something that we can render on Android
+  // natively. We fake native drawing of appearance: checkbox items
+  // out here, and use hardcoded colours from AndroidColors.h to
+  // simulate native theming.
+  int32_t appUnitsPerDevPixel = aFrame->PresContext()->AppUnitsPerDevPixel();
+  Rect devPxRect = NSRectToSnappedRect(rect, appUnitsPerDevPixel, *aDrawTarget);
+  aDrawTarget->StrokeRect(devPxRect,
+    ColorPattern(ToDeviceColor(mozilla::widget::sAndroidBorderColor)));
+}
+
+static void
+PaintCheckMark(nsIFrame* aFrame,
+               DrawTarget* aDrawTarget,
+               const nsRect& aDirtyRect,
+               nsPoint aPt)
+{
+  nsRect rect(aPt, aFrame->GetSize());
+  rect.Deflate(aFrame->GetUsedBorderAndPadding());
+
+  // Points come from the coordinates on a 7X7 unit box centered at 0,0
+  const int32_t checkPolygonX[] = { -3, -1,  3,  3, -1, -3 };
+  const int32_t checkPolygonY[] = { -1,  1, -3, -1,  3,  1 };
+  const int32_t checkNumPoints = sizeof(checkPolygonX) / sizeof(int32_t);
+  const int32_t checkSize      = 9; // 2 units of padding on either side
+                                    // of the 7x7 unit checkmark
+
+  // Scale the checkmark based on the smallest dimension
+  nscoord paintScale = std::min(rect.width, rect.height) / checkSize;
+  nsPoint paintCenter(rect.x + rect.width  / 2,
+                      rect.y + rect.height / 2);
+
+  RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder();
+  nsPoint p = paintCenter + nsPoint(checkPolygonX[0] * paintScale,
+                                    checkPolygonY[0] * paintScale);
+
+  int32_t appUnitsPerDevPixel = aFrame->PresContext()->AppUnitsPerDevPixel();
+  builder->MoveTo(NSPointToPoint(p, appUnitsPerDevPixel));
+  for (int32_t polyIndex = 1; polyIndex < checkNumPoints; polyIndex++) {
+    p = paintCenter + nsPoint(checkPolygonX[polyIndex] * paintScale,
+                              checkPolygonY[polyIndex] * paintScale);
+    builder->LineTo(NSPointToPoint(p, appUnitsPerDevPixel));
+  }
+  RefPtr<Path> path = builder->Finish();
+  aDrawTarget->Fill(path,
+    ColorPattern(ToDeviceColor(mozilla::widget::sAndroidCheckColor)));
+}
+
+static void
+PaintIndeterminateMark(nsIFrame* aFrame,
+                       DrawTarget* aDrawTarget,
+                       const nsRect& aDirtyRect,
+                       nsPoint aPt)
+{
+  int32_t appUnitsPerDevPixel = aFrame->PresContext()->AppUnitsPerDevPixel();
+
+  nsRect rect(aPt, aFrame->GetSize());
+  rect.Deflate(aFrame->GetUsedBorderAndPadding());
+  rect.y += (rect.height - rect.height/4) / 2;
+  rect.height /= 4;
+
+  Rect devPxRect = NSRectToSnappedRect(rect, appUnitsPerDevPixel, *aDrawTarget);
+  aDrawTarget->FillRect(devPxRect,
+    ColorPattern(ToDeviceColor(mozilla::widget::sAndroidCheckColor)));
+}
+
+void
+nsGfxCheckboxControlFrame::BuildDisplayList(nsDisplayListBuilder*   aBuilder,
+                                            const nsRect&           aDirtyRect,
+                                            const nsDisplayListSet& aLists)
+{
+  nsFormControlFrame::BuildDisplayList(aBuilder, aDirtyRect, aLists);
+
+  if (!IsVisibleForPainting(aBuilder)) {
+    return;   // nothing to paint.
+  }
+
+  if (IsThemed()) {
+    return; // No need to paint the checkmark. The theme will do it.
+  }
+
+  if (StyleDisplay()->mAppearance != NS_THEME_CHECKBOX) {
+    return;
+  }
+
+  aLists.Content()->AppendNewToTop(new (aBuilder)
+    nsDisplayGeneric(aBuilder, this, PaintCheckboxBorder,
+                     "CheckboxBorder", nsDisplayItem::TYPE_CHECKBOX_BORDER));
+
+  if (IsChecked() || IsIndeterminate()) {
+    aLists.Content()->AppendNewToTop(new (aBuilder)
+      nsDisplayGeneric(aBuilder, this,
+                       IsIndeterminate()
+                       ? PaintIndeterminateMark : PaintCheckMark,
+                       "CheckedCheckbox",
+                       nsDisplayItem::TYPE_CHECKED_CHECKBOX));
+  }
+}
+
+bool
+nsGfxCheckboxControlFrame::IsChecked()
+{
+  nsCOMPtr<nsIDOMHTMLInputElement> elem(do_QueryInterface(mContent));
+  bool retval = false;
+  elem->GetChecked(&retval);
+  return retval;
+}
+
+bool
+nsGfxCheckboxControlFrame::IsIndeterminate()
+{
+  nsCOMPtr<nsIDOMHTMLInputElement> elem(do_QueryInterface(mContent));
+  bool retval = false;
+  elem->GetIndeterminate(&retval);
+  return retval;
+}
+#endif
--- a/layout/forms/nsGfxCheckboxControlFrame.h
+++ b/layout/forms/nsGfxCheckboxControlFrame.h
@@ -17,15 +17,30 @@ public:
   virtual ~nsGfxCheckboxControlFrame();
 
 #ifdef DEBUG_FRAME_DUMP
   virtual nsresult GetFrameName(nsAString& aResult) const override {
     return MakeFrameName(NS_LITERAL_STRING("CheckboxControl"), aResult);
   }
 #endif
 
+#ifdef ANDROID
+  // On Android, there's no native theme or native widget support for
+  // checkbox or radio buttons. We draw them ourselves here using
+  // hardcoded colour values in order to simulate native drawing.
+  virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+                                const nsRect& aDirtyRect,
+                                const nsDisplayListSet& aLists) override;
+#endif
+
 #ifdef ACCESSIBILITY
   virtual mozilla::a11y::AccType AccessibleType() override;
 #endif
+
+#ifdef ANDROID
+protected:
+  bool IsChecked();
+  bool IsIndeterminate();
+#endif
 };
 
 #endif
 
--- a/layout/forms/nsGfxRadioControlFrame.cpp
+++ b/layout/forms/nsGfxRadioControlFrame.cpp
@@ -35,8 +35,95 @@ nsGfxRadioControlFrame::~nsGfxRadioContr
 
 #ifdef ACCESSIBILITY
 a11y::AccType
 nsGfxRadioControlFrame::AccessibleType()
 {
   return a11y::eHTMLRadioButtonType;
 }
 #endif
+
+#ifdef ANDROID
+
+#include "mozilla/widget/AndroidColors.h"
+
+static void
+PaintRadioBorder(nsIFrame* aFrame,
+                 DrawTarget* aDrawTarget,
+                 const nsRect& aDirtyRect,
+                 nsPoint aPt)
+{
+  nsRect rect(aPt, aFrame->GetSize());
+  rect.Deflate(aFrame->GetUsedBorderAndPadding());
+
+  Rect devPxRect =
+    ToRect(nsLayoutUtils::RectToGfxRect(rect,
+                                        aFrame->PresContext()->AppUnitsPerDevPixel()));
+  // Radio controls aren't something that we can render on Android
+  // natively. We fake native drawing of appearance: radio items
+  // out here, and use hardcoded colours to simulate native
+  // theming.
+  RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder();
+  AppendEllipseToPath(builder, devPxRect.Center(), devPxRect.Size());
+  RefPtr<Path> ellipse = builder->Finish();
+  aDrawTarget->Stroke(ellipse,
+    ColorPattern(ToDeviceColor(mozilla::widget::sAndroidBorderColor)));
+}
+
+//--------------------------------------------------------------
+// Draw the dot for a non-native radio button in the checked state.
+static void
+PaintCheckedRadioButton(nsIFrame* aFrame,
+                        DrawTarget* aDrawTarget,
+                        const nsRect& aDirtyRect,
+                        nsPoint aPt)
+{
+  // The dot is an ellipse 2px on all sides smaller than the content-box,
+  // drawn in the foreground color.
+  nsRect rect(aPt, aFrame->GetSize());
+  rect.Deflate(aFrame->GetUsedBorderAndPadding());
+  rect.Deflate(nsPresContext::CSSPixelsToAppUnits(2),
+               nsPresContext::CSSPixelsToAppUnits(2));
+
+  Rect devPxRect =
+    ToRect(nsLayoutUtils::RectToGfxRect(rect,
+                                        aFrame->PresContext()->AppUnitsPerDevPixel()));
+
+  RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder();
+  AppendEllipseToPath(builder, devPxRect.Center(), devPxRect.Size());
+  RefPtr<Path> ellipse = builder->Finish();
+  aDrawTarget->Fill(ellipse,
+    ColorPattern(ToDeviceColor(mozilla::widget::sAndroidCheckColor)));
+}
+
+void
+nsGfxRadioControlFrame::BuildDisplayList(nsDisplayListBuilder*   aBuilder,
+                                         const nsRect&           aDirtyRect,
+                                         const nsDisplayListSet& aLists)
+{
+  nsFormControlFrame::BuildDisplayList(aBuilder, aDirtyRect, aLists);
+
+  if (!IsVisibleForPainting(aBuilder)) {
+    return;
+  }
+
+  if (IsThemed()) {
+    return; // The theme will paint the check, if any.
+  }
+
+  if (StyleDisplay()->mAppearance != NS_THEME_RADIO) {
+    return;
+  }
+
+  aLists.Content()->AppendNewToTop(new (aBuilder)
+    nsDisplayGeneric(aBuilder, this, PaintRadioBorder,
+                     "RadioBorder", nsDisplayItem::TYPE_RADIOBUTTON_BORDER));
+
+  bool checked = true;
+  GetCurrentCheckState(&checked); // Get check state from the content model
+  if (checked) {
+    aLists.Content()->AppendNewToTop(new (aBuilder)
+      nsDisplayGeneric(aBuilder, this, PaintCheckedRadioButton,
+                       "CheckedRadioButton",
+                       nsDisplayItem::TYPE_CHECKED_RADIOBUTTON));
+  }
+}
+#endif
--- a/layout/forms/nsGfxRadioControlFrame.h
+++ b/layout/forms/nsGfxRadioControlFrame.h
@@ -18,11 +18,20 @@ public:
   explicit nsGfxRadioControlFrame(nsStyleContext* aContext);
   ~nsGfxRadioControlFrame();
 
   NS_DECL_FRAMEARENA_HELPERS
 
 #ifdef ACCESSIBILITY
   virtual mozilla::a11y::AccType AccessibleType() override;
 #endif
+
+#ifdef ANDROID
+  // On Android, there's no native theme or native widget support for
+  // checkbox or radio buttons. We draw them ourselves here using
+  // hardcoded colour values in order to simulate native drawing.
+  virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+                                const nsRect& aDirtyRect,
+                                const nsDisplayListSet& aLists) override;
+#endif
 };
 
 #endif
--- a/layout/painting/nsDisplayItemTypesList.h
+++ b/layout/painting/nsDisplayItemTypesList.h
@@ -13,16 +13,17 @@ DECLARE_DISPLAY_ITEM_TYPE(BUTTON_BORDER_
 DECLARE_DISPLAY_ITEM_TYPE(BUTTON_BOX_SHADOW_OUTER)
 DECLARE_DISPLAY_ITEM_TYPE(BUTTON_FOREGROUND)
 DECLARE_DISPLAY_ITEM_TYPE(CANVAS)
 DECLARE_DISPLAY_ITEM_TYPE(CANVAS_BACKGROUND_COLOR)
 DECLARE_DISPLAY_ITEM_TYPE(CANVAS_THEMED_BACKGROUND)
 DECLARE_DISPLAY_ITEM_TYPE(CANVAS_BACKGROUND_IMAGE)
 DECLARE_DISPLAY_ITEM_TYPE(CANVAS_FOCUS)
 DECLARE_DISPLAY_ITEM_TYPE(CARET)
+DECLARE_DISPLAY_ITEM_TYPE(CHECKBOX_BORDER)
 DECLARE_DISPLAY_ITEM_TYPE(CHECKED_CHECKBOX)
 DECLARE_DISPLAY_ITEM_TYPE(CHECKED_RADIOBUTTON)
 DECLARE_DISPLAY_ITEM_TYPE(CLEAR_BACKGROUND)
 DECLARE_DISPLAY_ITEM_TYPE(COLUMN_RULE)
 DECLARE_DISPLAY_ITEM_TYPE(COMBOBOX_FOCUS)
 DECLARE_DISPLAY_ITEM_TYPE(EVENT_RECEIVER)
 DECLARE_DISPLAY_ITEM_TYPE(LAYER_EVENT_REGIONS)
 DECLARE_DISPLAY_ITEM_TYPE(FIELDSET_BORDER_BACKGROUND)
@@ -36,16 +37,17 @@ DECLARE_DISPLAY_ITEM_TYPE(LIST_FOCUS)
 DECLARE_DISPLAY_ITEM_TYPE(OPACITY)
 DECLARE_DISPLAY_ITEM_TYPE(OPTION_EVENT_GRABBER)
 DECLARE_DISPLAY_ITEM_TYPE(OUTLINE)
 DECLARE_DISPLAY_ITEM_TYPE(OWN_LAYER)
 DECLARE_DISPLAY_ITEM_TYPE(PLUGIN)
 DECLARE_DISPLAY_ITEM_TYPE(PLUGIN_READBACK)
 DECLARE_DISPLAY_ITEM_TYPE(PLUGIN_VIDEO)
 DECLARE_DISPLAY_ITEM_TYPE(PRINT_PLUGIN)
+DECLARE_DISPLAY_ITEM_TYPE(RADIOBUTTON_BORDER)
 DECLARE_DISPLAY_ITEM_TYPE(RANGE_FOCUS_RING)
 DECLARE_DISPLAY_ITEM_TYPE(REMOTE)
 DECLARE_DISPLAY_ITEM_TYPE(RESOLUTION)
 DECLARE_DISPLAY_ITEM_TYPE(SCROLL_INFO_LAYER)
 DECLARE_DISPLAY_ITEM_TYPE(SELECTION_OVERLAY)
 DECLARE_DISPLAY_ITEM_TYPE(SOLID_COLOR)
 DECLARE_DISPLAY_ITEM_TYPE(SOLID_COLOR_REGION)
 DECLARE_DISPLAY_ITEM_TYPE(SUBDOCUMENT)
--- a/mobile/android/themes/core/content.css
+++ b/mobile/android/themes/core/content.css
@@ -93,17 +93,17 @@ select[size="1"] xul|scrollbarbutton {
   margin-left: 0;
   min-width: 16px;
 }
 
 /* Override inverse OS themes */
 textarea,
 button,
 xul|button,
-* > input:not([type="image"]) {
+* > input:not(:-moz-any([type="image"], [type="checkbox"], [type="radio"])) {
   -moz-appearance: none !important;  /* See bug 598421 for fixing the platform */
 }
 
 textarea,
 button,
 xul|button,
 * > input:not(:-moz-any([type="image"], [type="checkbox"], [type="radio"])) {
   border-radius: var(--form_border_radius);
new file mode 100644
--- /dev/null
+++ b/widget/android/AndroidColors.h
@@ -0,0 +1,15 @@
+#ifndef mozilla_widget_AndroidColors_h
+#define mozilla_widget_AndroidColors_h
+
+#include "mozilla/gfx/2D.h"
+
+namespace mozilla {
+namespace widget {
+
+static const Color sAndroidBorderColor(Color(0.73f, 0.73f, 0.73f));
+static const Color sAndroidCheckColor(Color(0.19f, 0.21f, 0.23f));
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
--- a/widget/android/moz.build
+++ b/widget/android/moz.build
@@ -20,16 +20,17 @@ EXPORTS += [
     'AndroidBridge.h',
     'AndroidJavaWrappers.h',
     'AndroidJNIWrapper.h',
     'GeneratedJNINatives.h',
     'GeneratedJNIWrappers.h',
 ]
 
 EXPORTS.mozilla.widget += [
+    'AndroidColors.h',
     'AndroidCompositorWidget.h',
     'AndroidUiThread.h',
 ]
 
 UNIFIED_SOURCES += [
     'AndroidAlerts.cpp',
     'AndroidBridge.cpp',
     'AndroidCompositorWidget.cpp',