/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsNativeThemeAndroid.h" #include "nsIFrame.h" #include "nsStyleConsts.h" #include "AndroidColors.h" #include "nsCSSRendering.h" #include "nsLayoutUtils.h" #include "PathHelpers.h" #include "mozilla/ClearOnShutdown.h" NS_IMPL_ISUPPORTS_INHERITED(nsNativeThemeAndroid, nsNativeTheme, nsITheme) using namespace mozilla; using namespace mozilla::gfx; static void ClampRectAndMoveToCenter(nsRect& aRect) { if (aRect.width < aRect.height) { aRect.y += (aRect.height - aRect.width) / 2; aRect.height = aRect.width; return; } if (aRect.height < aRect.width) { aRect.x += (aRect.width - aRect.height) / 2; aRect.width = aRect.height; } } static void PaintCheckboxControl(nsIFrame* aFrame, DrawTarget* aDrawTarget, const nsRect& aRect, const EventStates& aState) { // 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. RectCornerRadii innerRadii(2, 2, 2, 2); nsRect paddingRect = nsCSSRendering::GetBoxShadowInnerPaddingRect(aFrame, aRect); const nscoord twipsPerPixel = aFrame->PresContext()->DevPixelsToAppUnits(1); Rect shadowGfxRect = NSRectToRect(paddingRect, twipsPerPixel); shadowGfxRect.Round(); RefPtr roundedRect = MakePathForRoundedRect(*aDrawTarget, shadowGfxRect, innerRadii); aDrawTarget->Stroke( roundedRect, ColorPattern(ToDeviceColor(mozilla::widget::sAndroidBorderColor))); aDrawTarget->Fill( roundedRect, ColorPattern(ToDeviceColor(mozilla::widget::sAndroidBackgroundColor))); if (aState.HasState(NS_EVENT_STATE_DISABLED)) { aDrawTarget->Fill( roundedRect, ColorPattern(ToDeviceColor(mozilla::widget::sAndroidDisabledColor))); return; } if (aState.HasState(NS_EVENT_STATE_ACTIVE)) { aDrawTarget->Fill( roundedRect, ColorPattern(ToDeviceColor(mozilla::widget::sAndroidActiveColor))); } } static void PaintCheckMark(nsIFrame* aFrame, DrawTarget* aDrawTarget, const nsRect& aRect) { // 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(aRect.width, aRect.height) / checkSize; nsPoint paintCenter(aRect.x + aRect.width / 2, aRect.y + aRect.height / 2); RefPtr 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 = builder->Finish(); aDrawTarget->Fill( path, ColorPattern(ToDeviceColor(mozilla::widget::sAndroidCheckColor))); } static void PaintIndeterminateMark(nsIFrame* aFrame, DrawTarget* aDrawTarget, const nsRect& aRect) { int32_t appUnitsPerDevPixel = aFrame->PresContext()->AppUnitsPerDevPixel(); nsRect rect(aRect); 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))); } static void PaintRadioControl(nsIFrame* aFrame, DrawTarget* aDrawTarget, const nsRect& aRect, const EventStates& aState) { // 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. const nscoord twipsPerPixel = aFrame->PresContext()->DevPixelsToAppUnits(1); Rect devPxRect = NSRectToRect(aRect, twipsPerPixel); RefPtr builder = aDrawTarget->CreatePathBuilder(); AppendEllipseToPath(builder, devPxRect.Center(), devPxRect.Size()); RefPtr ellipse = builder->Finish(); aDrawTarget->Stroke( ellipse, ColorPattern(ToDeviceColor(mozilla::widget::sAndroidBorderColor))); aDrawTarget->Fill( ellipse, ColorPattern(ToDeviceColor(mozilla::widget::sAndroidBackgroundColor))); if (aState.HasState(NS_EVENT_STATE_DISABLED)) { aDrawTarget->Fill( ellipse, ColorPattern(ToDeviceColor(mozilla::widget::sAndroidDisabledColor))); return; } if (aState.HasState(NS_EVENT_STATE_ACTIVE)) { aDrawTarget->Fill( ellipse, ColorPattern(ToDeviceColor(mozilla::widget::sAndroidActiveColor))); } } static void PaintCheckedRadioButton(nsIFrame* aFrame, DrawTarget* aDrawTarget, const nsRect& aRect) { // The dot is an ellipse 2px on all sides smaller than the content-box, // drawn in the foreground color. nsRect rect(aRect); rect.Deflate(nsPresContext::CSSPixelsToAppUnits(2), nsPresContext::CSSPixelsToAppUnits(2)); Rect devPxRect = ToRect(nsLayoutUtils::RectToGfxRect( rect, aFrame->PresContext()->AppUnitsPerDevPixel())); RefPtr builder = aDrawTarget->CreatePathBuilder(); AppendEllipseToPath(builder, devPxRect.Center(), devPxRect.Size()); RefPtr ellipse = builder->Finish(); aDrawTarget->Fill( ellipse, ColorPattern(ToDeviceColor(mozilla::widget::sAndroidCheckColor))); } NS_IMETHODIMP nsNativeThemeAndroid::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance, const nsRect& aRect, const nsRect& aDirtyRect) { EventStates eventState = GetContentState(aFrame, aAppearance); nsRect rect(aRect); ClampRectAndMoveToCenter(rect); switch (aAppearance) { case StyleAppearance::Radio: PaintRadioControl(aFrame, aContext->GetDrawTarget(), rect, eventState); if (IsSelected(aFrame)) { PaintCheckedRadioButton(aFrame, aContext->GetDrawTarget(), rect); } break; case StyleAppearance::Checkbox: PaintCheckboxControl(aFrame, aContext->GetDrawTarget(), rect, eventState); if (IsChecked(aFrame)) { PaintCheckMark(aFrame, aContext->GetDrawTarget(), rect); } if (GetIndeterminate(aFrame)) { PaintIndeterminateMark(aFrame, aContext->GetDrawTarget(), rect); } break; default: MOZ_ASSERT_UNREACHABLE( "Should not get here with a widget type we don't support."); return NS_ERROR_NOT_IMPLEMENTED; } return NS_OK; } LayoutDeviceIntMargin nsNativeThemeAndroid::GetWidgetBorder( nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) { return LayoutDeviceIntMargin(); } bool nsNativeThemeAndroid::GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance, LayoutDeviceIntMargin* aResult) { switch (aAppearance) { // Radios and checkboxes return a fixed size in GetMinimumWidgetSize // and have a meaningful baseline, so they can't have // author-specified padding. case StyleAppearance::Checkbox: case StyleAppearance::Radio: aResult->SizeTo(0, 0, 0, 0); return true; default: return false; } } bool nsNativeThemeAndroid::GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance, nsRect* aOverflowRect) { return false; } NS_IMETHODIMP nsNativeThemeAndroid::GetMinimumWidgetSize(nsPresContext* aPresContext, nsIFrame* aFrame, StyleAppearance aAppearance, LayoutDeviceIntSize* aResult, bool* aIsOverridable) { if (aAppearance == StyleAppearance::Radio || aAppearance == StyleAppearance::Checkbox) { // 9px + (1px padding + 1px border) * 2 aResult->width = aPresContext->CSSPixelsToDevPixels(13); aResult->height = aPresContext->CSSPixelsToDevPixels(13); } return NS_OK; } NS_IMETHODIMP nsNativeThemeAndroid::WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance, nsAtom* aAttribute, bool* aShouldRepaint, const nsAttrValue* aOldValue) { if (aAppearance == StyleAppearance::Radio || aAppearance == StyleAppearance::Checkbox) { if (aAttribute == nsGkAtoms::active || aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::hover) { *aShouldRepaint = true; return NS_OK; } } *aShouldRepaint = false; return NS_OK; } NS_IMETHODIMP nsNativeThemeAndroid::ThemeChanged() { return NS_OK; } NS_IMETHODIMP_(bool) nsNativeThemeAndroid::ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame, StyleAppearance aAppearance) { switch (aAppearance) { case StyleAppearance::Radio: case StyleAppearance::Checkbox: return true; default: return false; } } NS_IMETHODIMP_(bool) nsNativeThemeAndroid::WidgetIsContainer(StyleAppearance aAppearance) { return false; } bool nsNativeThemeAndroid::ThemeDrawsFocusForWidget( StyleAppearance aAppearance) { return false; } bool nsNativeThemeAndroid::ThemeNeedsComboboxDropmarker() { return false; } nsITheme::Transparency nsNativeThemeAndroid::GetWidgetTransparency( nsIFrame* aFrame, StyleAppearance aAppearance) { return eUnknownTransparency; } already_AddRefed do_GetNativeTheme() { static nsCOMPtr inst; if (!inst) { inst = new nsNativeThemeAndroid(); ClearOnShutdown(&inst); } return do_AddRef(inst); }