blob: 8c92aa0057fb53b427693e6111c420b220e7f549 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// IMPORTANT NOTE: All QtShim members that use `delegate_` must be decorated
// with DISABLE_CFI_VCALL.
#include "ui/qt/qt_shim.h"
#include <cmath>
#include <QApplication>
#include <QFont>
#include <QIcon>
#include <QMimeDatabase>
#include <QMimeType>
#include <QPainter>
#include <QPalette>
#include <QScreen>
#include <QStyle>
#include <QStyleOptionTitleBar>
namespace qt {
namespace {
bool IsStyleItalic(QFont::Style style) {
switch (style) {
case QFont::Style::StyleNormal:
return false;
case QFont::Style::StyleItalic:
// gfx::Font::FontStyle doesn't support oblique, so treat it as italic.
case QFont::Style::StyleOblique:
return true;
}
}
FontHinting QtHintingToFontHinting(QFont::HintingPreference hinting) {
switch (hinting) {
case QFont::PreferDefaultHinting:
return FontHinting::kDefault;
case QFont::PreferNoHinting:
return FontHinting::kNone;
case QFont::PreferVerticalHinting:
// QT treats vertical hinting as "light" for Freetype:
// https://doc.qt.io/qt-5/qfont.html#HintingPreference-enum
return FontHinting::kLight;
case QFont::PreferFullHinting:
return FontHinting::kFull;
}
}
// Obtain the average color of a gradient.
SkColor GradientColor(const QGradient& gradient) {
QGradientStops stops = gradient.stops();
if (stops.empty()) {
return qRgba(0, 0, 0, 0);
}
float a = 0;
float r = 0;
float g = 0;
float b = 0;
for (int i = 0; i < stops.size(); i++) {
// Determine the extents of this stop. The whole gradient interval is [0,
// 1], so extend to the endpoint if this is the first or last stop.
float left_interval =
i == 0 ? stops[i].first : (stops[i].first - stops[i - 1].first) / 2;
float right_interval = i == stops.size() - 1
? 1 - stops[i].first
: (stops[i + 1].first - stops[i].first) / 2;
float length = left_interval + right_interval;
// alpha() returns a value in [0, 255] and alphaF() returns a value in
// [0, 1]. The color values are not premultiplied so the RGB channels need
// to be multiplied by the alpha (in range [0, 1]) before summing. The
// alpha doesn't need to be multiplied, so we just sum color.alpha() in
// range [0, 255] directly.
const QColor& color = stops[i].second;
a += color.alpha() * length;
r += color.alphaF() * color.red() * length;
g += color.alphaF() * color.green() * length;
b += color.alphaF() * color.blue() * length;
}
return qRgba(r, g, b, a);
}
// Obtain the average color of a texture.
SkColor TextureColor(QImage image) {
size_t size = image.width() * image.height();
if (!size) {
return qRgba(0, 0, 0, 0);
}
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
size_t a = 0;
size_t r = 0;
size_t g = 0;
size_t b = 0;
const auto* pixels = reinterpret_cast<QRgb*>(image.bits());
for (size_t i = 0; i < size; i++) {
auto color = QColor::fromRgba(pixels[i]);
a += color.alpha();
r += color.red();
g += color.green();
b += color.blue();
}
return qRgba(r / size, g / size, b / size, a / size);
}
SkColor BrushColor(const QBrush& brush) {
QColor color = brush.color();
auto alpha_blend = [&](uint8_t alpha) {
QColor blended = color;
blended.setAlpha(blended.alpha() * alpha / 255);
return blended.rgba();
};
switch (brush.style()) {
case Qt::SolidPattern:
return alpha_blend(0xFF);
case Qt::Dense1Pattern:
return alpha_blend(0xE0);
case Qt::Dense2Pattern:
return alpha_blend(0xC0);
case Qt::Dense3Pattern:
return alpha_blend(0xA0);
case Qt::Dense4Pattern:
return alpha_blend(0x80);
case Qt::Dense5Pattern:
return alpha_blend(0x60);
case Qt::Dense6Pattern:
return alpha_blend(0x40);
case Qt::Dense7Pattern:
return alpha_blend(0x20);
case Qt::NoBrush:
return alpha_blend(0x00);
case Qt::HorPattern:
case Qt::VerPattern:
return alpha_blend(0x20);
case Qt::CrossPattern:
return alpha_blend(0x40);
case Qt::BDiagPattern:
case Qt::FDiagPattern:
return alpha_blend(0x20);
case Qt::DiagCrossPattern:
return alpha_blend(0x40);
case Qt::LinearGradientPattern:
case Qt::RadialGradientPattern:
case Qt::ConicalGradientPattern:
return GradientColor(*brush.gradient());
case Qt::TexturePattern:
return TextureColor(brush.textureImage());
}
}
QPalette::ColorRole ColorTypeToColorRole(ColorType type) {
switch (type) {
case ColorType::kWindowBg:
return QPalette::Window;
case ColorType::kWindowFg:
return QPalette::WindowText;
case ColorType::kHighlightBg:
return QPalette::Highlight;
case ColorType::kHighlightFg:
return QPalette::HighlightedText;
case ColorType::kEntryBg:
return QPalette::Base;
case ColorType::kEntryFg:
return QPalette::Text;
case ColorType::kButtonBg:
return QPalette::Button;
case ColorType::kButtonFg:
return QPalette::ButtonText;
case ColorType::kLight:
return QPalette::Light;
case ColorType::kMidlight:
return QPalette::Midlight;
case ColorType::kMidground:
return QPalette::Mid;
case ColorType::kDark:
return QPalette::Dark;
case ColorType::kShadow:
return QPalette::Shadow;
}
}
QPalette::ColorGroup ColorStateToColorGroup(ColorState state) {
switch (state) {
case ColorState::kNormal:
return QPalette::Normal;
case ColorState::kDisabled:
return QPalette::Disabled;
case ColorState::kInactive:
return QPalette::Inactive;
}
}
float GetScreenScale(const QScreen* screen) {
constexpr double kDefaultPixelDpi = 96.0;
double scale = screen->devicePixelRatio() * screen->logicalDotsPerInch() /
kDefaultPixelDpi;
// Round to the nearest 1/16th so that UI can losslessly multiply and divide
// by the scale factor using floating point arithmetic. GtkUi also rounds
// in this way, but to 1/64th. 1/16th is chosen here since that's what
// KDE settings uses.
scale = std::round(scale * 16) / 16;
return scale > 0 ? scale : 1.0;
}
} // namespace
QtShim::QtShim(QtInterface::Delegate* delegate, int* argc, char** argv)
: delegate_(delegate), app_(*argc, argv) {
connect(&app_, SIGNAL(fontChanged(const QFont&)), this,
SLOT(FontChanged(const QFont&)));
connect(&app_, SIGNAL(paletteChanged(const QPalette&)), this,
SLOT(PaletteChanged(const QPalette&)));
connect(&app_, SIGNAL(screenAdded(QScreen*)), this,
SLOT(ScreenAdded(QScreen*)));
connect(&app_, SIGNAL(screenRemoved(QScreen*)), this,
SLOT(ScreenRemoved(QScreen*)));
for (QScreen* screen : app_.screens()) {
ScreenAdded(screen);
}
}
QtShim::~QtShim() = default;
size_t QtShim::GetMonitorConfig(MonitorScale** monitors, float* primary_scale) {
size_t n_monitors = app_.screens().size();
monitor_scales_.resize(n_monitors);
for (size_t i = 0; i < n_monitors; i++) {
const QScreen* screen = app_.screens()[i];
MonitorScale monitor = monitor_scales_[i];
monitor.x_px = screen->geometry().x();
monitor.y_px = screen->geometry().y();
monitor.width_px = screen->geometry().width();
monitor.height_px = screen->geometry().height();
monitor.scale = GetScreenScale(screen);
}
*monitors = monitor_scales_.data();
*primary_scale = GetScreenScale(app_.primaryScreen());
return n_monitors;
}
FontRenderParams QtShim::GetFontRenderParams() const {
QFont font = app_.font();
auto style = font.styleStrategy();
return {
.antialiasing = !(style & QFont::StyleStrategy::NoAntialias),
.use_bitmaps = !!(style & QFont::StyleStrategy::PreferBitmap),
.hinting = QtHintingToFontHinting(font.hintingPreference()),
};
}
FontDescription QtShim::GetFontDescription() const {
QFont font = app_.font();
return {
.family = String(font.family().toStdString().c_str()),
.size_pixels = font.pixelSize(),
.size_points = font.pointSize(),
.is_italic = IsStyleItalic(font.style()),
.weight = font.weight(),
};
}
Image QtShim::GetIconForContentType(const String& content_type,
int size) const {
QMimeDatabase db;
for (const char* mime : {content_type.c_str(), "application/octet-stream"}) {
auto mt = db.mimeTypeForName(mime);
for (const auto& name : {mt.iconName(), mt.genericIconName()}) {
auto icon = QIcon::fromTheme(name);
auto pixmap = icon.pixmap(size);
auto image = pixmap.toImage();
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
if (auto bytes = image.sizeInBytes()) {
return {image.width(), image.height(),
static_cast<float>(image.devicePixelRatio()),
Buffer(image.bits(), bytes)};
}
}
}
return {};
}
SkColor QtShim::GetColor(ColorType role, ColorState state) const {
return BrushColor(app_.palette().brush(ColorStateToColorGroup(state),
ColorTypeToColorRole(role)));
}
SkColor QtShim::GetFrameColor(ColorState state, bool use_custom_frame) const {
constexpr int kSampleSize = 32;
return TextureColor(DrawHeaderImpl(kSampleSize, kSampleSize,
GetColor(ColorType::kWindowBg, state),
state, use_custom_frame));
}
int QtShim::GetCursorBlinkIntervalMs() const {
return app_.cursorFlashTime();
}
int QtShim::GetAnimationDurationMs() const {
return app_.style()->styleHint(QStyle::SH_Widget_Animation_Duration);
}
DISABLE_CFI_VCALL
void QtShim::FontChanged(const QFont& font) {
delegate_->FontChanged();
}
DISABLE_CFI_VCALL
void QtShim::PaletteChanged(const QPalette& palette) {
delegate_->ThemeChanged();
}
DISABLE_CFI_VCALL
void QtShim::ScreenAdded(QScreen* screen) {
connect(screen, SIGNAL(logicalDotsPerInchChanged(qreal)), this,
SLOT(LogicalDotsPerInchChanged(qreal)));
connect(screen, SIGNAL(physicalDotsPerInchChanged(qreal)), this,
SLOT(PhysicalDotsPerInchChanged(qreal)));
delegate_->ScaleFactorMaybeChanged();
}
DISABLE_CFI_VCALL
void QtShim::ScreenRemoved(QScreen* screen) {
delegate_->ScaleFactorMaybeChanged();
}
DISABLE_CFI_VCALL
void QtShim::LogicalDotsPerInchChanged(qreal dpi) {
delegate_->ScaleFactorMaybeChanged();
}
DISABLE_CFI_VCALL
void QtShim::PhysicalDotsPerInchChanged(qreal dpi) {
delegate_->ScaleFactorMaybeChanged();
}
Image QtShim::DrawHeader(int width,
int height,
SkColor default_color,
ColorState state,
bool use_custom_frame) const {
QImage image =
DrawHeaderImpl(width, height, default_color, state, use_custom_frame);
return {width, height, 1.0f, Buffer(image.bits(), image.sizeInBytes())};
}
QImage QtShim::DrawHeaderImpl(int width,
int height,
SkColor default_color,
ColorState state,
bool use_custom_frame) const {
QImage image(width, height, QImage::Format_ARGB32_Premultiplied);
image.fill(default_color);
QPainter painter(&image);
if (use_custom_frame) {
// Chrome renders it's own window border, so clip the border out by
// rendering the titlebar larger than the image.
constexpr int kBorderWidth = 5;
QStyleOptionTitleBar opt;
opt.rect = QRect(-kBorderWidth, -kBorderWidth, width + 2 * kBorderWidth,
height + 2 * kBorderWidth);
if (state == ColorState::kNormal) {
opt.titleBarState = QStyle::State_Active;
}
app_.style()->drawComplexControl(QStyle::CC_TitleBar, &opt, &painter,
nullptr);
} else {
painter.fillRect(
0, 0, width, height,
app_.palette().brush(ColorStateToColorGroup(state), QPalette::Window));
}
return image;
}
} // namespace qt
qt::QtInterface* CreateQtInterface(qt::QtInterface::Delegate* delegate,
int* argc,
char** argv) {
return new qt::QtShim(delegate, argc, argv);
}