/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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 "nsNativeThemeGTK.h" #include "cairo.h" #include "nsDeviceContext.h" #include "gtk/gtk.h" #include "nsPresContext.h" #include "GtkWidgets.h" #include "nsIFrame.h" #include "gfxContext.h" #include "mozilla/gfx/HelpersCairo.h" #include "mozilla/WidgetUtilsGtk.h" #include "mozilla/StaticPrefs_widget.h" #include using namespace mozilla; using namespace mozilla::gfx; using namespace mozilla::widget; nsNativeThemeGTK::nsNativeThemeGTK() : Theme(ScrollbarStyle()) {} nsNativeThemeGTK::~nsNativeThemeGTK() { GtkWidgets::Shutdown(); } // This is easy to extend to 9-patch if we ever paint native widgets // again, but we are very unlikely to do that. static RefPtr GetWidgetFourPatch( nsIFrame* aFrame, GtkWidgets::Type aWidget, CSSIntCoord aSectionSize, CSSToLayoutDeviceScale aScale) { static auto sCairoSurfaceSetDeviceScalePtr = (void (*)(cairo_surface_t*, double, double))dlsym( RTLD_DEFAULT, "cairo_surface_set_device_scale"); CSSIntRect rect(0, 0, aSectionSize * 2, aSectionSize * 2); // Save actual widget scale to GtkWidgetState as we don't provide // the frame to gtk3drawing routines. GtkWidgets::DrawingParams params{ .widget = aWidget, .rect = {rect.x, rect.y, rect.width, rect.height}, .state = GTK_STATE_FLAG_NORMAL, .image_scale = gint(std::ceil(aScale.scale)), }; if (aFrame->PresContext()->Document()->State().HasState( dom::DocumentState::WINDOW_INACTIVE)) { params.state = GtkStateFlags(gint(params.state) | GTK_STATE_FLAG_BACKDROP); } auto surfaceRect = RoundedOut(rect * aScale); RefPtr dataSurface = Factory::CreateDataSourceSurface( surfaceRect.Size().ToUnknownSize(), SurfaceFormat::B8G8R8A8, /* aZero = */ true); if (NS_WARN_IF(!dataSurface)) { return nullptr; } DataSourceSurface::ScopedMap map(dataSurface, DataSourceSurface::MapType::WRITE); if (NS_WARN_IF(!map.IsMapped())) { return nullptr; } // Create a Cairo image surface wrapping the data surface. cairo_surface_t* surf = cairo_image_surface_create_for_data( map.GetData(), GfxFormatToCairoFormat(dataSurface->GetFormat()), surfaceRect.width, surfaceRect.height, map.GetStride()); if (NS_WARN_IF(!surf)) { return nullptr; } if (cairo_t* cr = cairo_create(surf)) { if (aScale.scale != 1.0) { if (sCairoSurfaceSetDeviceScalePtr) { sCairoSurfaceSetDeviceScalePtr(surf, aScale.scale, aScale.scale); } else { cairo_scale(cr, aScale.scale, aScale.scale); } } GtkWidgets::Draw(cr, ¶ms); cairo_destroy(cr); } cairo_surface_destroy(surf); return dataSurface; } static void DrawWindowDecorationsWithCairo(nsIFrame* aFrame, gfxContext* aContext, bool aSnapped, const Point& aDrawOrigin, const nsIntSize& aDrawSize) { DrawTarget* dt = aContext->GetDrawTarget(); // If we are not snapped, we depend on the DT for translation. // Otherwise, we only need to take the device offset into account. const Point drawOffset = aSnapped ? aDrawOrigin - dt->GetTransform().GetTranslation() - aContext->GetDeviceOffset() : aDrawOrigin; const CSSIntCoord sectionSize = LookAndFeel::GetInt(LookAndFeel::IntID::TitlebarRadius); if (!sectionSize) { return; } const CSSToLayoutDeviceScale scaleFactor{ float(AppUnitsPerCSSPixel()) / float(aFrame->PresContext() ->DeviceContext() ->AppUnitsPerDevPixelAtUnitFullZoom())}; RefPtr dataSurface = GetWidgetFourPatch( aFrame, GtkWidgets::Type::WindowDecoration, sectionSize, scaleFactor); if (NS_WARN_IF(!dataSurface)) { return; } LayoutDeviceSize scaledSize(CSSCoord(sectionSize) * scaleFactor, CSSCoord(sectionSize) * scaleFactor); // Top left. dt->DrawSurface(dataSurface, Rect(drawOffset, scaledSize.ToUnknownSize()), Rect(Point(), scaledSize.ToUnknownSize())); // Top right. dt->DrawSurface(dataSurface, Rect(Point(drawOffset.x + aDrawSize.width - scaledSize.width, drawOffset.y), scaledSize.ToUnknownSize()), Rect(Point(scaledSize.width, 0), scaledSize.ToUnknownSize())); if (StaticPrefs::widget_gtk_rounded_bottom_corners_enabled()) { // Bottom left. dt->DrawSurface( dataSurface, Rect(Point(drawOffset.x, drawOffset.y + aDrawSize.height - scaledSize.height), scaledSize.ToUnknownSize()), Rect(Point(0, scaledSize.height), scaledSize.ToUnknownSize())); // Bottom right dt->DrawSurface( dataSurface, Rect(Point(drawOffset.x + aDrawSize.width - scaledSize.width, drawOffset.y + aDrawSize.height - scaledSize.height), scaledSize.ToUnknownSize()), Rect(Point(scaledSize.width, scaledSize.height), scaledSize.ToUnknownSize())); } } void nsNativeThemeGTK::DrawWidgetBackground( gfxContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance, const nsRect& aRect, const nsRect& aDirtyRect, DrawOverflow aDrawOverflow) { if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { return Theme::DrawWidgetBackground(aContext, aFrame, aAppearance, aRect, aDirtyRect, aDrawOverflow); } if (NS_WARN_IF(aAppearance != StyleAppearance::MozWindowDecorations)) { return; } if (GdkIsWaylandDisplay()) { // We don't need to paint window decorations on Wayland, see the comments in // browser.css return; } gfxContext* ctx = aContext; nsPresContext* presContext = aFrame->PresContext(); gfxRect rect = presContext->AppUnitsToGfxUnits(aRect); gfxRect dirtyRect = presContext->AppUnitsToGfxUnits(aDirtyRect); // Align to device pixels where sensible // to provide crisper and faster drawing. // Don't snap if it's a non-unit scale factor. We're going to have to take // slow paths then in any case. // We prioritize the size when snapping in order to avoid distorting widgets // that should be square, which can occur if edges are snapped independently. bool snapped = ctx->UserToDevicePixelSnapped( rect, gfxContext::SnapOption::PrioritizeSize); if (snapped) { // Leave rect in device coords but make dirtyRect consistent. dirtyRect = ctx->UserToDevice(dirtyRect); } // Translate the dirty rect so that it is wrt the widget top-left. dirtyRect.MoveBy(-rect.TopLeft()); // Round out the dirty rect to gdk pixels to ensure that gtk draws // enough pixels for interpolation to device pixels. dirtyRect.RoundOut(); // GTK themes can only draw an integer number of pixels // (even when not snapped). LayoutDeviceIntRect widgetRect(0, 0, NS_lround(rect.Width()), NS_lround(rect.Height())); // This is the rectangle that will actually be drawn, in gdk pixels LayoutDeviceIntRect drawingRect( int32_t(dirtyRect.X()), int32_t(dirtyRect.Y()), int32_t(dirtyRect.Width()), int32_t(dirtyRect.Height())); if (widgetRect.IsEmpty() || !drawingRect.IntersectRect(widgetRect, drawingRect)) { return; } // translate everything so (0,0) is the top left of the drawingRect gfxPoint origin = rect.TopLeft() + drawingRect.TopLeft().ToUnknownPoint(); DrawWindowDecorationsWithCairo(aFrame, ctx, snapped, ToPoint(origin), drawingRect.Size().ToUnknownSize()); } bool nsNativeThemeGTK::CreateWebRenderCommandsForWidget( mozilla::wr::DisplayListBuilder& aBuilder, mozilla::wr::IpcResourceUpdateQueue& aResources, const mozilla::layers::StackingContextHelper& aSc, mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame, StyleAppearance aAppearance, const nsRect& aRect) { if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { return Theme::CreateWebRenderCommandsForWidget( aBuilder, aResources, aSc, aManager, aFrame, aAppearance, aRect); } if (aAppearance == StyleAppearance::MozWindowDecorations && GdkIsWaylandDisplay()) { // On wayland we don't need to draw window decorations. return true; } return false; } LayoutDeviceIntMargin nsNativeThemeGTK::GetWidgetBorder( nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) { if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { return Theme::GetWidgetBorder(aContext, aFrame, aAppearance); } return {}; } bool nsNativeThemeGTK::GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance, LayoutDeviceIntMargin* aResult) { if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { return Theme::GetWidgetPadding(aContext, aFrame, aAppearance, aResult); } return false; } bool nsNativeThemeGTK::GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance, nsRect* aOverflowRect) { if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { return Theme::GetWidgetOverflow(aContext, aFrame, aAppearance, aOverflowRect); } return false; } auto nsNativeThemeGTK::IsWidgetNonNative(nsIFrame* aFrame, StyleAppearance aAppearance) -> NonNative { if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { return NonNative::Always; } // If the current GTK theme color scheme matches our color-scheme, then we // can draw a native widget. if (LookAndFeel::ColorSchemeForFrame(aFrame) == PreferenceSheet::ColorSchemeForChrome()) { return NonNative::No; } // If the non-native theme doesn't support the widget then oh well... if (!Theme::ThemeSupportsWidget(aFrame->PresContext(), aFrame, aAppearance)) { return NonNative::No; } return NonNative::BecauseColorMismatch; } bool nsNativeThemeGTK::IsWidgetAlwaysNonNative(nsIFrame* aFrame, StyleAppearance aAppearance) { return Theme::IsWidgetAlwaysNonNative(aFrame, aAppearance) || aAppearance == StyleAppearance::MozMenulistArrowButton || aAppearance == StyleAppearance::Textfield || aAppearance == StyleAppearance::NumberInput || aAppearance == StyleAppearance::PasswordInput || aAppearance == StyleAppearance::Textarea || aAppearance == StyleAppearance::Checkbox || aAppearance == StyleAppearance::Radio || aAppearance == StyleAppearance::Button || aAppearance == StyleAppearance::Listbox || aAppearance == StyleAppearance::Menulist; } LayoutDeviceIntSize nsNativeThemeGTK::GetMinimumWidgetSize( nsPresContext* aPresContext, nsIFrame* aFrame, StyleAppearance aAppearance) { if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { return Theme::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance); } return {}; } bool nsNativeThemeGTK::ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame, StyleAppearance aAppearance) { if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { return Theme::ThemeSupportsWidget(aPresContext, aFrame, aAppearance); } return aAppearance == StyleAppearance::MozWindowDecorations; } bool nsNativeThemeGTK::ThemeDrawsFocusForWidget(nsIFrame* aFrame, StyleAppearance aAppearance) { if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { return Theme::ThemeDrawsFocusForWidget(aFrame, aAppearance); } return false; } nsITheme::Transparency nsNativeThemeGTK::GetWidgetTransparency( nsIFrame* aFrame, StyleAppearance aAppearance) { if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { return Theme::GetWidgetTransparency(aFrame, aAppearance); } return eUnknownTransparency; } already_AddRefed do_CreateNativeThemeDoNotUseDirectly() { if (gfxPlatform::IsHeadless()) { return do_AddRef(new Theme(Theme::ScrollbarStyle())); } return do_AddRef(new nsNativeThemeGTK()); }