1
0
Fork 0
firefox/widget/gtk/nsNativeThemeGTK.cpp
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

532 lines
19 KiB
C++

/* -*- 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 <algorithm>
#include <dlfcn.h>
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<WidgetNodeType> 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<SystemCairoClipper> 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<SystemCairoClipper> 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<DataSourceSurface> 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<Theme> do_CreateNativeThemeDoNotUseDirectly() {
if (gfxPlatform::IsHeadless()) {
return do_AddRef(new Theme(Theme::ScrollbarStyle()));
}
return do_AddRef(new nsNativeThemeGTK());
}