UI/Qt+WebContent: Refresh theme colors live

Refresh Qt chrome palettes from the current color scheme when the
application palette changes. Recompute autocomplete and location field
colors and repaint WebContent immediately so theme changes do not
require another interaction or restart.
This commit is contained in:
Andreas Kling 2026-05-25 21:22:50 +02:00 committed by Andreas Kling
parent 3adeed9581
commit 7a4a633d04
10 changed files with 234 additions and 41 deletions

View file

@ -150,8 +150,11 @@ Gfx::Palette PageClient::palette() const
void PageClient::set_palette_impl(Gfx::PaletteImpl& impl)
{
m_palette_impl = impl;
if (auto* document = page().top_level_browsing_context().active_document())
if (auto* document = page().top_level_browsing_context().active_document()) {
document->invalidate_style(Web::DOM::StyleInvalidationReason::SettingsChange);
document->set_needs_media_query_evaluation();
}
request_frame();
}
void PageClient::set_preferred_color_scheme(Web::CSS::PreferredColorScheme color_scheme)

View file

@ -28,6 +28,7 @@
#include <QPixmap>
#include <QPoint>
#include <QStyledItemDelegate>
#include <QTimer>
#include <QVBoxLayout>
namespace Ladybird {
@ -87,6 +88,21 @@ static QFont autocomplete_section_header_font()
return font;
}
static QColor autocomplete_selection_fill(QPalette const& palette)
{
auto surface = ChromeStyle::chrome_surface(palette);
return ChromeStyle::is_dark(palette)
? ChromeStyle::mix(surface, QColor(92, 112, 140), 0.54)
: ChromeStyle::mix(surface, palette.color(QPalette::Highlight), 0.10);
}
static QColor autocomplete_selection_border(QPalette const& palette)
{
return ChromeStyle::is_dark(palette)
? ChromeStyle::mix(autocomplete_selection_fill(palette), QColor(160, 176, 198), 0.32)
: ChromeStyle::mix(ChromeStyle::chrome_border(palette), palette.color(QPalette::Highlight), 0.12);
}
class AutocompleteModel final : public QAbstractListModel {
public:
explicit AutocompleteModel(QObject* parent)
@ -257,7 +273,7 @@ public:
if (kind == RowKind::SectionHeader) {
auto text = index.data(HeaderTextRole).toString();
painter->setFont(autocomplete_section_header_font());
auto header_color = option.palette.color(QPalette::PlaceholderText);
auto header_color = ChromeStyle::chrome_muted_text(option.palette);
header_color.setAlpha(170);
painter->setPen(header_color);
auto rect = option.rect.adjusted(
@ -270,14 +286,10 @@ public:
bool selected = option.state & QStyle::State_Selected;
if (selected) {
auto fill = ChromeStyle::chrome_surface_hover(option.palette);
fill.setAlpha(190);
auto border = ChromeStyle::chrome_border(option.palette);
border.setAlpha(76);
auto rect = option.rect.adjusted(3, 2, -3, -2);
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setPen(QPen(border, 1));
painter->setBrush(fill);
painter->setPen(QPen(autocomplete_selection_border(option.palette), 1));
painter->setBrush(autocomplete_selection_fill(option.palette));
painter->drawRoundedRect(rect, 7, 7);
}
@ -314,13 +326,13 @@ public:
int block_y = option.rect.top() + (option.rect.height() - block_height) / 2;
painter->setFont(autocomplete_primary_font());
painter->setPen(option.palette.color(QPalette::Text));
painter->setPen(ChromeStyle::chrome_text(option.palette));
auto elided_title = primary_fm.elidedText(title_text, Qt::ElideRight, text_width);
painter->drawText(QRect(text_x, block_y, text_width, primary_fm.height()),
Qt::AlignLeft | Qt::AlignVCenter, elided_title);
painter->setFont(autocomplete_secondary_font());
auto secondary_color = option.palette.color(QPalette::PlaceholderText);
auto secondary_color = ChromeStyle::chrome_muted_text(option.palette);
secondary_color.setAlpha(180);
painter->setPen(secondary_color);
auto elided_secondary = secondary_fm.elidedText(secondary_text, Qt::ElideRight, text_width);
@ -330,7 +342,7 @@ public:
Qt::AlignLeft | Qt::AlignVCenter, elided_secondary);
} else {
painter->setFont(QApplication::font());
painter->setPen(option.palette.color(QPalette::Text));
painter->setPen(ChromeStyle::chrome_text(option.palette));
QFontMetrics fm(QApplication::font());
auto elided_url = fm.elidedText(url_text, Qt::ElideRight, text_width);
painter->drawText(
@ -356,7 +368,6 @@ Autocomplete::Autocomplete(QLineEdit* anchor)
m_popup->setFrameShape(QFrame::StyledPanel);
m_popup->setFrameShadow(QFrame::Raised);
m_popup->setAutoFillBackground(true);
m_popup->setStyleSheet(ChromeStyle::autocomplete_popup_style_sheet(m_anchor->palette()));
m_popup->hide();
m_list_view = new QListView(m_popup);
@ -372,6 +383,7 @@ Autocomplete::Autocomplete(QLineEdit* anchor)
m_delegate = new AutocompleteDelegate(this);
m_list_view->setModel(m_model);
m_list_view->setItemDelegate(m_delegate);
update_chrome_style();
auto* layout = new QVBoxLayout(m_popup);
layout->setContentsMargins(0, POPUP_PADDING, 0, POPUP_PADDING);
@ -416,6 +428,33 @@ void Autocomplete::cancel_pending_query()
m_autocomplete->cancel_pending_query();
}
void Autocomplete::update_chrome_style()
{
if (m_is_updating_chrome_style)
return;
m_is_updating_chrome_style = true;
auto palette = m_anchor->palette();
m_popup->setPalette(palette);
m_list_view->setPalette(palette);
m_popup->setStyleSheet(ChromeStyle::autocomplete_popup_style_sheet(palette));
m_popup->update();
m_list_view->viewport()->update();
m_is_updating_chrome_style = false;
}
void Autocomplete::schedule_chrome_style_update()
{
if (m_has_pending_chrome_style_update)
return;
m_has_pending_chrome_style_update = true;
QTimer::singleShot(0, this, [this] {
m_has_pending_chrome_style_update = false;
update_chrome_style();
});
}
void Autocomplete::show_with_suggestions(Vector<WebView::AutocompleteSuggestion> suggestions, int selected_suggestion_index)
{
m_model->set_suggestions(move(suggestions));
@ -424,7 +463,7 @@ void Autocomplete::show_with_suggestions(Vector<WebView::AutocompleteSuggestion>
return;
}
m_popup->setStyleSheet(ChromeStyle::autocomplete_popup_style_sheet(m_anchor->palette()));
update_chrome_style();
position_popup();
if (!m_popup->isVisible())
m_popup->show();
@ -514,6 +553,11 @@ bool Autocomplete::select_previous_suggestion()
bool Autocomplete::eventFilter(QObject* watched, QEvent* event)
{
auto type = event->type();
if (type == QEvent::ApplicationPaletteChange || type == QEvent::ThemeChange
|| (type == QEvent::PaletteChange && (watched == m_anchor || watched == m_popup || watched == m_list_view)))
schedule_chrome_style_update();
if (event->type() == QEvent::MouseButtonPress && is_visible()) {
auto* mouse_event = static_cast<QMouseEvent*>(event);
auto global = mouse_event->globalPosition().toPoint();

View file

@ -37,6 +37,8 @@ public:
void query_autocomplete_engine(String);
void cancel_pending_query();
void update_chrome_style();
void schedule_chrome_style_update();
void show_with_suggestions(Vector<WebView::AutocompleteSuggestion>, int selected_suggestion_index);
bool close();
@ -66,6 +68,8 @@ private:
QListView* m_list_view { nullptr };
AutocompleteModel* m_model { nullptr };
AutocompleteDelegate* m_delegate { nullptr };
bool m_is_updating_chrome_style { false };
bool m_has_pending_chrome_style_update { false };
NonnullOwnPtr<WebView::Autocomplete> m_autocomplete;
};

View file

@ -6,14 +6,75 @@
#include <UI/Qt/ChromeStyle.h>
#include <QGuiApplication>
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
# include <QStyleHints>
#endif
namespace Ladybird::ChromeStyle {
static bool is_dark(QPalette const& palette)
static bool color_is_dark(QColor const& color)
{
return palette.color(QPalette::Window).lightness() < 128;
return color.lightness() < 128;
}
static QColor mix(QColor const& from, QColor const& to, double amount)
static bool palette_is_dark(QPalette const& palette)
{
return color_is_dark(palette.color(QPalette::Window));
}
bool is_dark(QPalette const& palette)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
auto color_scheme = QGuiApplication::styleHints()->colorScheme();
if (color_scheme != Qt::ColorScheme::Unknown)
return color_scheme == Qt::ColorScheme::Dark;
#endif
return palette_is_dark(palette);
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
static bool palette_roles_match_color_scheme(QPalette const& palette, bool dark)
{
return color_is_dark(palette.color(QPalette::Window)) == dark
&& color_is_dark(palette.color(QPalette::Base)) == dark
&& color_is_dark(palette.color(QPalette::Text)) != dark
&& color_is_dark(palette.color(QPalette::ButtonText)) != dark;
}
#endif
static bool palette_matches_current_color_scheme(QPalette const& palette)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
auto color_scheme = QGuiApplication::styleHints()->colorScheme();
if (color_scheme == Qt::ColorScheme::Dark)
return palette_roles_match_color_scheme(palette, true);
if (color_scheme == Qt::ColorScheme::Light)
return palette_roles_match_color_scheme(palette, false);
#endif
Q_UNUSED(palette);
return true;
}
static QColor chrome_window(QPalette const& palette)
{
if (palette_matches_current_color_scheme(palette))
return palette.color(QPalette::Window);
return is_dark(palette) ? QColor(26, 29, 36) : QColor(244, 246, 248);
}
static QColor chrome_base(QPalette const& palette)
{
if (palette_matches_current_color_scheme(palette))
return palette.color(QPalette::Base);
return is_dark(palette) ? QColor(24, 29, 38) : QColor(255, 255, 255);
}
QColor mix(QColor const& from, QColor const& to, double amount)
{
auto channel = [&](int from_channel, int to_channel) {
return static_cast<int>(from_channel + (to_channel - from_channel) * amount);
@ -28,7 +89,7 @@ static QColor mix(QColor const& from, QColor const& to, double amount)
QColor chrome_background(QPalette const& palette)
{
auto window = palette.color(QPalette::Window);
auto window = chrome_window(palette);
return is_dark(palette)
? mix(window, QColor(10, 16, 24), 0.72)
: mix(window, QColor(241, 245, 249), 0.62);
@ -36,7 +97,7 @@ QColor chrome_background(QPalette const& palette)
QColor chrome_surface(QPalette const& palette)
{
auto base = palette.color(QPalette::Base);
auto base = chrome_base(palette);
return is_dark(palette)
? mix(base, QColor(31, 39, 52), 0.64)
: mix(base, QColor(255, 255, 255), 0.72);
@ -70,8 +131,27 @@ QColor chrome_accent(QPalette const& palette)
return palette.color(QPalette::Highlight);
}
QColor chrome_text(QPalette const& palette)
{
if (palette_matches_current_color_scheme(palette))
return palette.color(QPalette::Text);
return is_dark(palette) ? QColor(238, 241, 246) : QColor(24, 29, 36);
}
QColor chrome_button_text(QPalette const& palette)
{
if (palette_matches_current_color_scheme(palette))
return palette.color(QPalette::ButtonText);
return chrome_text(palette);
}
QColor chrome_muted_text(QPalette const& palette)
{
if (!palette_matches_current_color_scheme(palette))
return is_dark(palette) ? QColor(154, 163, 176) : QColor(98, 108, 122);
return palette.color(QPalette::PlaceholderText);
}
@ -88,7 +168,7 @@ QString navigation_toolbar_style_sheet(QPalette const& palette)
auto surface_pressed = style_sheet_color(chrome_surface_pressed(palette));
auto border = style_sheet_color(chrome_border(palette));
auto separator = style_sheet_color(mix(chrome_background(palette), chrome_border(palette), is_dark(palette) ? 0.28 : 0.56));
auto text = style_sheet_color(palette.color(QPalette::ButtonText));
auto text = style_sheet_color(chrome_button_text(palette));
auto disabled_text = style_sheet_color(chrome_muted_text(palette));
return QStringLiteral(R"(
@ -143,7 +223,7 @@ QString location_edit_style_sheet(QPalette const& palette)
auto hover = style_sheet_color(hover_color);
auto border = style_sheet_color(chrome_border(palette));
auto focus_border = style_sheet_color(mix(chrome_border(palette), chrome_accent(palette), is_dark(palette) ? 0.46 : 0.58));
auto text = style_sheet_color(palette.color(QPalette::Text));
auto text = style_sheet_color(chrome_text(palette));
auto placeholder = style_sheet_color(chrome_muted_text(palette));
auto selection = style_sheet_color(chrome_accent(palette));
auto selection_text = style_sheet_color(palette.color(QPalette::HighlightedText));
@ -199,7 +279,7 @@ QString bookmarks_bar_style_sheet(QPalette const& palette)
auto hover = style_sheet_color(chrome_surface_hover(palette));
auto pressed = style_sheet_color(chrome_surface_pressed(palette));
auto border = style_sheet_color(chrome_border(palette));
auto text = style_sheet_color(palette.color(QPalette::ButtonText));
auto text = style_sheet_color(chrome_button_text(palette));
return QStringLiteral(R"(
QToolBar#LadybirdBookmarksBar {
@ -241,7 +321,7 @@ QString find_in_page_style_sheet(QPalette const& palette)
auto pressed = style_sheet_color(chrome_surface_pressed(palette));
auto border = style_sheet_color(chrome_border(palette));
auto accent = style_sheet_color(chrome_accent(palette));
auto text = style_sheet_color(palette.color(QPalette::Text));
auto text = style_sheet_color(chrome_text(palette));
auto muted = style_sheet_color(chrome_muted_text(palette));
return QStringLiteral(R"(
@ -297,7 +377,7 @@ QString tab_widget_style_sheet(QPalette const& palette)
auto hover = style_sheet_color(chrome_surface_hover(palette));
auto pressed = style_sheet_color(chrome_surface_pressed(palette));
auto border = style_sheet_color(chrome_border(palette));
auto text = style_sheet_color(palette.color(QPalette::ButtonText));
auto text = style_sheet_color(chrome_button_text(palette));
auto close_hover = style_sheet_color(QColor(196, 43, 28));
auto close_text = style_sheet_color(QColor(255, 255, 255));
@ -379,7 +459,7 @@ QString autocomplete_popup_style_sheet(QPalette const& palette)
{
auto surface = style_sheet_color(chrome_surface(palette));
auto border = style_sheet_color(chrome_border(palette));
auto text = style_sheet_color(palette.color(QPalette::Text));
auto text = style_sheet_color(chrome_text(palette));
return QStringLiteral(R"(
QFrame#LadybirdAutocompletePopup {

View file

@ -12,6 +12,10 @@
namespace Ladybird::ChromeStyle {
bool is_dark(QPalette const&);
QColor mix(QColor const& from, QColor const& to, double amount);
QColor chrome_text(QPalette const&);
QColor chrome_button_text(QPalette const&);
QColor chrome_background(QPalette const&);
QColor chrome_surface(QPalette const&);
QColor chrome_surface_hover(QPalette const&);

View file

@ -5,6 +5,7 @@
*/
#include <LibCore/Resource.h>
#include <UI/Qt/ChromeStyle.h>
#include <UI/Qt/Icon.h>
#include <UI/Qt/StringUtils.h>
#include <UI/Qt/TVGIconEngine.h>
@ -198,12 +199,17 @@ static QIcon create_y_offset_icon(QIcon const& source, int y_offset)
QIcon create_chrome_icon(ChromeIcon icon, QPalette const& palette)
{
if (icon == ChromeIcon::Reload)
return create_y_offset_icon(create_tvg_icon_with_theme_colors("reload", palette), 1);
if (icon == ChromeIcon::Globe)
return create_y_offset_icon(create_tvg_icon_with_theme_colors("globe", palette, 202, 96, 236, 236), 1);
auto chrome_palette = palette;
chrome_palette.setColor(QPalette::Normal, QPalette::ButtonText, ChromeStyle::chrome_button_text(palette));
chrome_palette.setColor(QPalette::Active, QPalette::ButtonText, ChromeStyle::chrome_button_text(palette));
chrome_palette.setColor(QPalette::Disabled, QPalette::ButtonText, ChromeStyle::chrome_muted_text(palette));
auto normal = palette.color(QPalette::ColorGroup::Normal, QPalette::ColorRole::ButtonText);
if (icon == ChromeIcon::Reload)
return create_y_offset_icon(create_tvg_icon_with_theme_colors("reload", chrome_palette), 1);
if (icon == ChromeIcon::Globe)
return create_y_offset_icon(create_tvg_icon_with_theme_colors("globe", chrome_palette, 202, 96, 236, 236), 1);
auto normal = ChromeStyle::chrome_button_text(palette);
auto normal_alpha = 216;
if (icon == ChromeIcon::Close)
normal_alpha = 172;
@ -211,10 +217,10 @@ QIcon create_chrome_icon(ChromeIcon icon, QPalette const& palette)
normal_alpha = 204;
normal.setAlpha(normal_alpha);
auto active = palette.color(QPalette::ColorGroup::Active, QPalette::ColorRole::ButtonText);
auto active = ChromeStyle::chrome_button_text(palette);
active.setAlpha(icon == ChromeIcon::Close ? 220 : 236);
auto disabled = palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::ButtonText);
auto disabled = ChromeStyle::chrome_muted_text(palette);
disabled.setAlpha(icon == ChromeIcon::Close ? 78 : 96);
QIcon qicon;
@ -237,7 +243,7 @@ QIcon loading_spinner_icon(QPalette const& palette, int frame)
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(icon_size / 2.0, icon_size / 2.0);
auto color = palette.color(QPalette::Text);
auto color = ChromeStyle::chrome_text(palette);
for (int segment = 0; segment < segment_count; ++segment) {
auto segment_color = color;
segment_color.setAlpha(((segment - frame + segment_count) % segment_count + 1) * 255 / segment_count);

View file

@ -20,10 +20,14 @@
#include <QApplication>
#include <QEasingCurve>
#include <QGraphicsDropShadowEffect>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QLatin1String>
#include <QPalette>
#include <QResizeEvent>
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
# include <QStyleHints>
#endif
#include <QTextLayout>
#include <QTimer>
#include <QToolButton>
@ -33,7 +37,7 @@ namespace Ladybird {
static QColor location_focus_glow_color(QPalette const& palette, int alpha)
{
auto color = palette.color(QPalette::Highlight);
auto color = ChromeStyle::chrome_accent(palette);
color.setAlpha(alpha);
return color;
}
@ -270,6 +274,12 @@ LocationEdit::LocationEdit(QWidget* parent)
highlight_location();
update_location_icon();
});
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, [this] {
schedule_chrome_style_update();
});
#endif
}
void LocationEdit::set_trailing_action(QAction* action)
@ -278,13 +288,16 @@ void LocationEdit::set_trailing_action(QAction* action)
m_trailing_action_button->setVisible(action != nullptr);
}
QAction* LocationEdit::trailing_action() const
{
return m_trailing_action_button->defaultAction();
}
void LocationEdit::changeEvent(QEvent* event)
{
QLineEdit::changeEvent(event);
if (event->type() == QEvent::PaletteChange) {
update_chrome_style();
update_focus_glow(m_focus_glow_alpha);
update_location_icon();
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::ApplicationPaletteChange || event->type() == QEvent::ThemeChange) {
schedule_chrome_style_update();
}
}
@ -402,6 +415,23 @@ void LocationEdit::update_chrome_style()
m_is_updating_chrome_style = false;
}
void LocationEdit::schedule_chrome_style_update()
{
if (m_has_pending_chrome_style_update)
return;
m_has_pending_chrome_style_update = true;
QTimer::singleShot(0, this, [this] {
m_has_pending_chrome_style_update = false;
update_chrome_style();
m_autocomplete->schedule_chrome_style_update();
update_focus_glow(m_focus_glow_alpha);
update_location_icon();
highlight_location();
update();
});
}
void LocationEdit::update_placeholder()
{
if (auto const& search_engine = WebView::Application::settings().search_engine(); search_engine.has_value()) {
@ -447,14 +477,14 @@ void LocationEdit::highlight_location()
QList<QInputMethodEvent::Attribute> attributes;
if (auto url_parts = WebView::break_url_into_parts(url); url_parts.has_value()) {
auto darkened_text_color = QPalette().color(QPalette::Text);
auto darkened_text_color = ChromeStyle::chrome_text(palette());
darkened_text_color.setAlpha(127);
QTextCharFormat dark_attributes;
dark_attributes.setForeground(darkened_text_color);
QTextCharFormat highlight_attributes;
highlight_attributes.setForeground(QPalette().color(QPalette::Text));
highlight_attributes.setForeground(ChromeStyle::chrome_text(palette()));
attributes.append({
QInputMethodEvent::TextFormat,

View file

@ -37,6 +37,7 @@ public:
explicit LocationEdit(QWidget*);
void set_trailing_action(QAction*);
QAction* trailing_action() const;
Optional<URL::URL const&> url() const { return m_url; }
void set_url(Optional<URL::URL>);
@ -60,6 +61,7 @@ private:
void update_location_icon();
void update_loading_icon();
void update_focus_glow(int alpha);
void schedule_chrome_style_update();
void animate_focus_glow(int target_alpha);
void highlight_location();
bool text_matches_current_url() const;
@ -83,6 +85,7 @@ private:
bool m_url_is_hidden { false };
bool m_is_loading { false };
bool m_is_updating_chrome_style { false };
bool m_has_pending_chrome_style_update { false };
int m_focus_glow_alpha { 0 };
int m_loading_animation_frame { 0 };

View file

@ -674,6 +674,10 @@ void Tab::recreate_toolbar_icons()
m_reload_action->setIcon(create_chrome_icon(ChromeIcon::Reload, palette()));
m_window->new_tab_action().setIcon(create_chrome_icon(ChromeIcon::NewTab, palette()));
m_hamburger_button->setIcon(create_chrome_icon(ChromeIcon::Menu, palette()));
if (auto* action = m_location_edit->trailing_action()) {
auto icon = view().toggle_bookmark_action().engaged() ? ChromeIcon::StarFilled : ChromeIcon::Star;
action->setIcon(create_chrome_icon(icon, palette()));
}
}
void Tab::show_find_in_page()

View file

@ -40,6 +40,9 @@
#include <QPainter>
#include <QPalette>
#include <QScrollBar>
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
# include <QStyleHints>
#endif
#include <QTextEdit>
#include <QTimer>
#include <QToolTip>
@ -71,6 +74,15 @@ WebContentView::WebContentView(QWidget* window, RefPtr<WebView::WebContentClient
update_screen_rects();
});
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
QObject::connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, [this] {
QTimer::singleShot(0, this, [this] {
update_palette();
update();
});
});
#endif
m_tooltip_hover_timer.setSingleShot(true);
QObject::connect(&m_tooltip_hover_timer, &QTimer::timeout, [this] {
@ -784,8 +796,11 @@ bool WebContentView::event(QEvent* event)
return true;
}
if (event->type() == QEvent::PaletteChange) {
update_palette();
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::ApplicationPaletteChange || event->type() == QEvent::ThemeChange) {
QTimer::singleShot(0, this, [this] {
update_palette();
update();
});
return QWidget::event(event);
}