Source code

Revision control

Copy as Markdown

Other Tools

/* 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 <dlfcn.h>
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<DataSourceSurface> 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<DataSourceSurface> 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, &params);
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<Theme> do_CreateNativeThemeDoNotUseDirectly() {
if (gfxPlatform::IsHeadless()) {
return do_AddRef(new Theme(Theme::ScrollbarStyle()));
}
return do_AddRef(new nsNativeThemeGTK());
}