Ladybird: Use custom QIconEngine to render scalable/vector icons

Rather than render the icons to a 16x16 bitmap, keep them as vector
graphics and render them on request. This keeps the icons crisp on high
DPI displays.
This commit is contained in:
MacDue 2023-07-29 19:46:47 +01:00 committed by Andreas Kling
parent 3e6ca1085c
commit 1837e94ba4
Notes: sideshowbarker 2024-07-17 05:09:48 +09:00
5 changed files with 170 additions and 31 deletions

View file

@ -91,6 +91,7 @@ set(SOURCES
Settings.cpp Settings.cpp
SettingsDialog.cpp SettingsDialog.cpp
Tab.cpp Tab.cpp
TVGIconEngine.cpp
Utilities.cpp Utilities.cpp
WebContentView.cpp WebContentView.cpp
ladybird.qrc ladybird.qrc

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/MemoryStream.h>
#include <AK/String.h>
#include <LibGfx/Painter.h>
#include <QFile>
#include <QImage>
#include <QPainter>
#include <QPixmapCache>
#include "TVGIconEngine.h"
#include "Utilities.h"
void TVGIconEngine::paint(QPainter* qpainter, QRect const& rect, QIcon::Mode mode, QIcon::State state)
{
qpainter->drawPixmap(rect, pixmap(rect.size(), mode, state));
}
QIconEngine* TVGIconEngine::clone() const
{
return new TVGIconEngine(*this);
}
QPixmap TVGIconEngine::pixmap(QSize const& size, QIcon::Mode mode, QIcon::State state)
{
QPixmap pixmap;
auto key = pixmap_cache_key(size, mode, state);
if (QPixmapCache::find(key, &pixmap))
return pixmap;
auto bitmap = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { size.width(), size.height() }));
Gfx::Painter painter { *bitmap };
auto icon_rect = m_image_data->rect().to_type<float>();
auto scale = min(size.width() / icon_rect.width(), size.height() / icon_rect.height()) * m_scale;
auto centered = Gfx::FloatRect { {}, icon_rect.size().scaled_by(scale) }
.centered_within(Gfx::FloatRect { {}, { size.width(), size.height() } });
auto transform = Gfx::AffineTransform {}
.translate(centered.location())
.multiply(Gfx::AffineTransform {}.scale(scale, scale));
m_image_data->draw_transformed(painter, transform);
for (auto const& filter : m_filters) {
if (filter->mode() == mode) {
painter.blit_filtered({}, *bitmap, bitmap->rect(), filter->function(), false);
break;
}
}
QImage qimage { bitmap->scanline_u8(0), bitmap->width(), bitmap->height(), QImage::Format::Format_ARGB32 };
pixmap = QPixmap::fromImage(qimage);
if (!pixmap.isNull())
QPixmapCache::insert(key, pixmap);
return pixmap;
}
QString TVGIconEngine::pixmap_cache_key(QSize const& size, QIcon::Mode mode, QIcon::State state)
{
return qstring_from_ak_string(
MUST(String::formatted("$sernity_tvgicon_{}_{}x{}_{}_{}", m_cache_id, size.width(), size.height(), to_underlying(mode), to_underlying(state))));
}
void TVGIconEngine::add_filter(QIcon::Mode mode, Function<Color(Color)> filter)
{
m_filters.empend(adopt_ref(*new Filter(mode, move(filter))));
invalidate_cache();
}
TVGIconEngine* TVGIconEngine::from_file(QString const& path)
{
QFile icon_resource(path);
if (!icon_resource.open(QIODeviceBase::ReadOnly))
return nullptr;
auto icon_data = icon_resource.readAll();
FixedMemoryStream icon_bytes { ReadonlyBytes { icon_data.data(), static_cast<size_t>(icon_data.size()) } };
if (auto tvg = Gfx::TinyVGDecodedImageData::decode(icon_bytes); !tvg.is_error())
return new TVGIconEngine(tvg.release_value());
return nullptr;
}

71
Ladybird/TVGIconEngine.h Normal file
View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <AK/RefCounted.h>
#include <AK/Vector.h>
#include <LibGfx/ImageFormats/TinyVGLoader.h>
#include <QIconEngine>
class TVGIconEngine : public QIconEngine {
public:
TVGIconEngine(Gfx::TinyVGDecodedImageData const& image_data)
: m_image_data(image_data)
{
}
static TVGIconEngine* from_file(QString const& path);
void paint(QPainter* painter, QRect const& rect, QIcon::Mode mode,
QIcon::State state) override;
QIconEngine* clone() const override;
QPixmap pixmap(QSize const& size, QIcon::Mode mode,
QIcon::State state) override;
void add_filter(QIcon::Mode mode, Function<Color(Color)> filter);
void set_scale(float scale)
{
m_scale = scale;
invalidate_cache();
}
private:
static unsigned next_cache_id()
{
static unsigned cache_id = 0;
return cache_id++;
}
void invalidate_cache()
{
m_cache_id = next_cache_id();
}
class Filter : public RefCounted<Filter> {
public:
Filter(QIcon::Mode mode, Function<Color(Color)> function)
: m_mode(mode)
, m_function(move(function))
{
}
QIcon::Mode mode() const { return m_mode; }
Function<Color(Color)> const& function() const { return m_function; }
private:
QIcon::Mode m_mode;
Function<Color(Color)> m_function;
};
QString pixmap_cache_key(QSize const& size, QIcon::Mode mode, QIcon::State state);
float m_scale { 1 };
Vector<NonnullRefPtr<Filter>> m_filters;
NonnullRefPtr<Gfx::TinyVGDecodedImageData> m_image_data;
unsigned m_cache_id { next_cache_id() };
};

View file

@ -9,6 +9,7 @@
#include "ConsoleWidget.h" #include "ConsoleWidget.h"
#include "InspectorWidget.h" #include "InspectorWidget.h"
#include "Settings.h" #include "Settings.h"
#include "TVGIconEngine.h"
#include "Utilities.h" #include "Utilities.h"
#include <Browser/History.h> #include <Browser/History.h>
#include <LibGfx/ImageFormats/BMPWriter.h> #include <LibGfx/ImageFormats/BMPWriter.h>
@ -31,33 +32,20 @@
extern DeprecatedString s_serenity_resource_root; extern DeprecatedString s_serenity_resource_root;
extern Browser::Settings* s_settings; extern Browser::Settings* s_settings;
static QIcon render_tvg_icon_with_theme_colors(QString name, QPalette const& palette) static QIcon create_tvg_icon_with_theme_colors(QString name, QPalette const& palette)
{ {
auto path = QString(":/Icons/%1.tvg").arg(name); auto path = QString(":/Icons/%1.tvg").arg(name);
Gfx::IntSize icon_size(16, 16); auto icon_engine = TVGIconEngine::from_file(path);
VERIFY(icon_engine);
QFile icon_resource(path); auto icon_filter = [](QColor color) {
VERIFY(icon_resource.open(QIODeviceBase::ReadOnly)); return [color = Color::from_argb(color.rgba64().toArgb32())](Gfx::Color icon_color) {
auto icon_data = icon_resource.readAll(); return color.with_alpha((icon_color.alpha() * color.alpha()) / 255);
ReadonlyBytes icon_bytes { icon_data.data(), static_cast<size_t>(icon_data.size()) }; };
auto icon_raster = MUST(Gfx::Bitmap::load_from_bytes(icon_bytes, icon_size));
QIcon icon;
auto render = [&](QColor color) -> QPixmap {
auto image = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, icon_size));
Gfx::Painter painter { image };
auto icon_color = Color::from_argb(color.rgba64().toArgb32());
painter.blit_filtered({ 0, 0 }, *icon_raster, icon_raster->rect(), [&](auto color) {
return icon_color.with_alpha((icon_color.alpha() * color.alpha()) / 255);
});
QImage qimage { image->scanline_u8(0), image->width(), image->height(), QImage::Format::Format_ARGB32 };
return QPixmap::fromImage(qimage);
}; };
icon_engine->add_filter(QIcon::Mode::Normal, icon_filter(palette.color(QPalette::ColorGroup::Normal, QPalette::ColorRole::ButtonText)));
icon.addPixmap(render(palette.color(QPalette::ColorGroup::Normal, QPalette::ColorRole::ButtonText)), QIcon::Mode::Normal); icon_engine->add_filter(QIcon::Mode::Disabled, icon_filter(palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::ButtonText)));
icon.addPixmap(render(palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::ButtonText)), QIcon::Mode::Disabled); icon_engine->set_scale(0.66f);
return QIcon(icon_engine);
return icon;
} }
Tab::Tab(BrowserWindow* window, StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling enable_callgrind_profiling, WebView::UseJavaScriptBytecode use_javascript_bytecode) Tab::Tab(BrowserWindow* window, StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling enable_callgrind_profiling, WebView::UseJavaScriptBytecode use_javascript_bytecode)
@ -85,7 +73,7 @@ Tab::Tab(BrowserWindow* window, StringView webdriver_content_ipc_path, WebView::
m_layout->addWidget(m_toolbar); m_layout->addWidget(m_toolbar);
m_layout->addWidget(m_view); m_layout->addWidget(m_view);
rerender_toolbar_icons(); recreate_toolbar_icons();
m_toolbar->addAction(&m_window->go_back_action()); m_toolbar->addAction(&m_window->go_back_action());
m_toolbar->addAction(&m_window->go_forward_action()); m_toolbar->addAction(&m_window->go_forward_action());
@ -614,18 +602,18 @@ void Tab::update_hover_label()
bool Tab::event(QEvent* event) bool Tab::event(QEvent* event)
{ {
if (event->type() == QEvent::PaletteChange) { if (event->type() == QEvent::PaletteChange) {
rerender_toolbar_icons(); recreate_toolbar_icons();
return QWidget::event(event); return QWidget::event(event);
} }
return QWidget::event(event); return QWidget::event(event);
} }
void Tab::rerender_toolbar_icons() void Tab::recreate_toolbar_icons()
{ {
m_window->go_back_action().setIcon(render_tvg_icon_with_theme_colors("back", palette())); m_window->go_back_action().setIcon(create_tvg_icon_with_theme_colors("back", palette()));
m_window->go_forward_action().setIcon(render_tvg_icon_with_theme_colors("forward", palette())); m_window->go_forward_action().setIcon(create_tvg_icon_with_theme_colors("forward", palette()));
m_window->reload_action().setIcon(render_tvg_icon_with_theme_colors("reload", palette())); m_window->reload_action().setIcon(create_tvg_icon_with_theme_colors("reload", palette()));
} }
void Tab::show_inspector_window(InspectorTarget inspector_target) void Tab::show_inspector_window(InspectorTarget inspector_target)

View file

@ -68,7 +68,7 @@ private:
virtual void resizeEvent(QResizeEvent*) override; virtual void resizeEvent(QResizeEvent*) override;
virtual bool event(QEvent*) override; virtual bool event(QEvent*) override;
void rerender_toolbar_icons(); void recreate_toolbar_icons();
void update_hover_label(); void update_hover_label();
void open_link(URL const&); void open_link(URL const&);