/* -*- 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 "nsDeviceContext.h" #include "gtk/gtk.h" #include "nsPresContext.h" #include "gtkdrawing.h" #include "nsIFrame.h" #include "gfxContext.h" #include "mozilla/gfx/BorrowedContext.h" #include "mozilla/gfx/HelpersCairo.h" #include "mozilla/gfx/PathHelpers.h" #include "mozilla/WidgetUtilsGtk.h" #ifdef MOZ_X11 # ifdef CAIRO_HAS_XLIB_SURFACE # include "cairo-xlib.h" # endif #endif #include #include using namespace mozilla; using namespace mozilla::gfx; using namespace mozilla::widget; // Return widget scale factor of the monitor where the window is located by the // most part. We intentionally honor the text scale factor here in order to // have consistent scaling with other UI elements, except for the window // decorations, which should use unscaled pixels. static inline CSSToLayoutDeviceScale GetWidgetScaleFactor( nsIFrame* aFrame, StyleAppearance aAppearance) { if (aAppearance == StyleAppearance::MozWindowDecorations) { // Window decorations can't honor the text scale. return CSSToLayoutDeviceScale{ float(AppUnitsPerCSSPixel()) / float(aFrame->PresContext() ->DeviceContext() ->AppUnitsPerDevPixelAtUnitFullZoom())}; } return aFrame->PresContext()->CSSToDevPixelScale(); } nsNativeThemeGTK::nsNativeThemeGTK() : Theme(ScrollbarStyle()) { moz_gtk_init(); } nsNativeThemeGTK::~nsNativeThemeGTK() { moz_gtk_shutdown(); } static Maybe GeckoToGtkWidgetType(StyleAppearance aAppearance) { switch (aAppearance) { case StyleAppearance::MozWindowDecorations: return Some(MOZ_GTK_WINDOW_DECORATION); default: MOZ_ASSERT_UNREACHABLE("Unknown widget"); break; } return {}; } class SystemCairoClipper : public ClipExporter { public: explicit SystemCairoClipper(cairo_t* aContext, gint aScaleFactor = 1) : mContext(aContext), mScaleFactor(aScaleFactor) {} void BeginClip(const Matrix& aTransform) override { cairo_matrix_t mat; GfxMatrixToCairoMatrix(aTransform, mat); // We also need to remove the scale factor effect from the matrix mat.y0 = mat.y0 / mScaleFactor; mat.x0 = mat.x0 / mScaleFactor; cairo_set_matrix(mContext, &mat); cairo_new_path(mContext); } void MoveTo(const Point& aPoint) override { cairo_move_to(mContext, aPoint.x / mScaleFactor, aPoint.y / mScaleFactor); mBeginPoint = aPoint; mCurrentPoint = aPoint; } void LineTo(const Point& aPoint) override { cairo_line_to(mContext, aPoint.x / mScaleFactor, aPoint.y / mScaleFactor); mCurrentPoint = aPoint; } void BezierTo(const Point& aCP1, const Point& aCP2, const Point& aCP3) override { cairo_curve_to(mContext, aCP1.x / mScaleFactor, aCP1.y / mScaleFactor, aCP2.x / mScaleFactor, aCP2.y / mScaleFactor, aCP3.x / mScaleFactor, aCP3.y / mScaleFactor); mCurrentPoint = aCP3; } void QuadraticBezierTo(const Point& aCP1, const Point& aCP2) override { Point CP0 = CurrentPoint(); Point CP1 = (CP0 + aCP1 * 2.0) / 3.0; Point CP2 = (aCP2 + aCP1 * 2.0) / 3.0; Point CP3 = aCP2; cairo_curve_to(mContext, CP1.x / mScaleFactor, CP1.y / mScaleFactor, CP2.x / mScaleFactor, CP2.y / mScaleFactor, CP3.x / mScaleFactor, CP3.y / mScaleFactor); mCurrentPoint = aCP2; } void Arc(const Point& aOrigin, float aRadius, float aStartAngle, float aEndAngle, bool aAntiClockwise) override { ArcToBezier(this, aOrigin, Size(aRadius, aRadius), aStartAngle, aEndAngle, aAntiClockwise); } void Close() override { cairo_close_path(mContext); mCurrentPoint = mBeginPoint; } void EndClip() override { cairo_clip(mContext); } private: cairo_t* mContext; gint mScaleFactor; }; static void DrawThemeWithCairo(gfxContext* aContext, DrawTarget* aDrawTarget, const GtkDrawingParams& aParams, double aScaleFactor, bool aSnapped, const Point& aDrawOrigin, const nsIntSize& aDrawSize, nsITheme::Transparency aTransparency) { static auto sCairoSurfaceSetDeviceScalePtr = (void (*)(cairo_surface_t*, double, double))dlsym( RTLD_DEFAULT, "cairo_surface_set_device_scale"); const bool useHiDPIWidgets = aScaleFactor != 1.0 && sCairoSurfaceSetDeviceScalePtr; Point drawOffsetScaled; Point drawOffsetOriginal; Matrix transform; if (!aSnapped) { // If we are not snapped, we depend on the DT for translation. drawOffsetOriginal = aDrawOrigin; drawOffsetScaled = useHiDPIWidgets ? drawOffsetOriginal / aScaleFactor : drawOffsetOriginal; transform = aDrawTarget->GetTransform().PreTranslate(drawOffsetScaled); } else { // Otherwise, we only need to take the device offset into account. drawOffsetOriginal = aDrawOrigin - aContext->GetDeviceOffset(); drawOffsetScaled = useHiDPIWidgets ? drawOffsetOriginal / aScaleFactor : drawOffsetOriginal; transform = Matrix::Translation(drawOffsetScaled); } if (!useHiDPIWidgets && aScaleFactor != 1) { transform.PreScale(aScaleFactor, aScaleFactor); } cairo_matrix_t mat; GfxMatrixToCairoMatrix(transform, mat); Size clipSize((aDrawSize.width + aScaleFactor - 1) / aScaleFactor, (aDrawSize.height + aScaleFactor - 1) / aScaleFactor); // A direct Cairo draw target is not available, so we need to create a // temporary one. #if defined(MOZ_X11) && defined(CAIRO_HAS_XLIB_SURFACE) if (GdkIsX11Display()) { // If using a Cairo xlib surface, then try to reuse it. BorrowedXlibDrawable borrow(aDrawTarget); if (Drawable drawable = borrow.GetDrawable()) { nsIntSize size = borrow.GetSize(); cairo_surface_t* surf = cairo_xlib_surface_create( borrow.GetDisplay(), drawable, borrow.GetVisual(), size.width, size.height); if (!NS_WARN_IF(!surf)) { Point offset = borrow.GetOffset(); if (offset != Point()) { cairo_surface_set_device_offset(surf, offset.x, offset.y); } cairo_t* cr = cairo_create(surf); if (!NS_WARN_IF(!cr)) { RefPtr clipper = new SystemCairoClipper(cr); aContext->ExportClip(*clipper); cairo_set_matrix(cr, &mat); cairo_new_path(cr); cairo_rectangle(cr, 0, 0, clipSize.width, clipSize.height); cairo_clip(cr); moz_gtk_widget_paint(cr, &aParams); cairo_destroy(cr); } cairo_surface_destroy(surf); } borrow.Finish(); return; } } #endif // Check if the widget requires complex masking that must be composited. // Try to directly write to the draw target's pixels if possible. uint8_t* data; nsIntSize size; int32_t stride; SurfaceFormat format; IntPoint origin; if (aDrawTarget->LockBits(&data, &size, &stride, &format, &origin)) { // Create a Cairo image surface context the device rectangle. cairo_surface_t* surf = cairo_image_surface_create_for_data( data, GfxFormatToCairoFormat(format), size.width, size.height, stride); if (!NS_WARN_IF(!surf)) { if (useHiDPIWidgets) { sCairoSurfaceSetDeviceScalePtr(surf, aScaleFactor, aScaleFactor); } if (origin != IntPoint()) { cairo_surface_set_device_offset(surf, -origin.x, -origin.y); } cairo_t* cr = cairo_create(surf); if (!NS_WARN_IF(!cr)) { RefPtr clipper = new SystemCairoClipper(cr, useHiDPIWidgets ? aScaleFactor : 1); aContext->ExportClip(*clipper); cairo_set_matrix(cr, &mat); cairo_new_path(cr); cairo_rectangle(cr, 0, 0, clipSize.width, clipSize.height); cairo_clip(cr); moz_gtk_widget_paint(cr, &aParams); cairo_destroy(cr); } cairo_surface_destroy(surf); } aDrawTarget->ReleaseBits(data); } else { // If the widget has any transparency, make sure to choose an alpha format. format = aTransparency != nsITheme::eOpaque ? SurfaceFormat::B8G8R8A8 : aDrawTarget->GetFormat(); // Create a temporary data surface to render the widget into. RefPtr dataSurface = Factory::CreateDataSourceSurface( aDrawSize, format, aTransparency != nsITheme::eOpaque); DataSourceSurface::MappedSurface map; if (!NS_WARN_IF( !(dataSurface && dataSurface->Map(DataSourceSurface::MapType::WRITE, &map)))) { // Create a Cairo image surface wrapping the data surface. cairo_surface_t* surf = cairo_image_surface_create_for_data( map.mData, GfxFormatToCairoFormat(format), aDrawSize.width, aDrawSize.height, map.mStride); cairo_t* cr = nullptr; if (!NS_WARN_IF(!surf)) { cr = cairo_create(surf); if (!NS_WARN_IF(!cr)) { if (aScaleFactor != 1) { if (useHiDPIWidgets) { sCairoSurfaceSetDeviceScalePtr(surf, aScaleFactor, aScaleFactor); } else { cairo_scale(cr, aScaleFactor, aScaleFactor); } } moz_gtk_widget_paint(cr, &aParams); } } // Unmap the surface before using it as a source dataSurface->Unmap(); if (cr) { // The widget either needs to be masked or has transparency, so use the // slower drawing path. aDrawTarget->DrawSurface( dataSurface, Rect(aSnapped ? drawOffsetOriginal - aDrawTarget->GetTransform().GetTranslation() : drawOffsetOriginal, Size(aDrawSize)), Rect(0, 0, aDrawSize.width, aDrawSize.height)); cairo_destroy(cr); } if (surf) { cairo_surface_destroy(surf); } } } } 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); } auto gtkType = GeckoToGtkWidgetType(aAppearance); if (!gtkType) { 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; } Transparency transparency = GetWidgetTransparency(aFrame, aAppearance); // gdk rectangles are wrt the drawing rect. auto scaleFactor = GetWidgetScaleFactor(aFrame, aAppearance); LayoutDeviceIntRect gdkDevRect(-drawingRect.TopLeft(), widgetRect.Size()); auto gdkCssRect = CSSIntRect::RoundIn(gdkDevRect / scaleFactor); GdkRectangle gdk_rect = {gdkCssRect.x, gdkCssRect.y, gdkCssRect.width, gdkCssRect.height}; // Save actual widget scale to GtkWidgetState as we don't provide // the frame to gtk3drawing routines. GtkDrawingParams params{ .widget = *gtkType, .rect = gdk_rect, .state = GTK_STATE_FLAG_NORMAL, .image_scale = gint(std::ceil(scaleFactor.scale)), }; if (aFrame->PresContext()->Document()->State().HasState( dom::DocumentState::WINDOW_INACTIVE)) { params.state = GtkStateFlags(gint(params.state) | GTK_STATE_FLAG_BACKDROP); } // translate everything so (0,0) is the top left of the drawingRect gfxPoint origin = rect.TopLeft() + drawingRect.TopLeft().ToUnknownPoint(); DrawThemeWithCairo(ctx, aContext->GetDrawTarget(), params, scaleFactor.scale, snapped, ToPoint(origin), drawingRect.Size().ToUnknownSize(), transparency); } 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::Toolbarbutton || aAppearance == StyleAppearance::Listbox || aAppearance == StyleAppearance::Menulist || aAppearance == StyleAppearance::ProgressBar || aAppearance == StyleAppearance::Progresschunk || aAppearance == StyleAppearance::Range || aAppearance == StyleAppearance::RangeThumb; } LayoutDeviceIntSize nsNativeThemeGTK::GetMinimumWidgetSize( nsPresContext* aPresContext, nsIFrame* aFrame, StyleAppearance aAppearance) { if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { return Theme::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance); } return {}; } bool nsNativeThemeGTK::WidgetAttributeChangeRequiresRepaint( StyleAppearance aAppearance, nsAtom* aAttribute) { // Some widget types just never change state. if (aAppearance == StyleAppearance::MozWindowDecorations) { return false; } return Theme::WidgetAttributeChangeRequiresRepaint(aAppearance, aAttribute); } bool nsNativeThemeGTK::ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame, StyleAppearance aAppearance) { if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { return Theme::ThemeSupportsWidget(aPresContext, aFrame, aAppearance); } switch (aAppearance) { case StyleAppearance::MozWindowDecorations: return !IsWidgetStyled(aPresContext, aFrame, aAppearance); default: break; } return false; } bool nsNativeThemeGTK::WidgetIsContainer(StyleAppearance aAppearance) { // XXXdwh At some point flesh all of this out. return true; } 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()); }