// Copyright 2016 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/gtk/native_theme_gtk.h" #include "base/no_destructor.h" #include "base/ranges/algorithm.h" #include "base/strings/strcat.h" #include "cc/paint/paint_canvas.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/color/color_id.h" #include "ui/color/color_provider.h" #include "ui/color/color_provider_manager.h" #include "ui/gfx/color_palette.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/skia_conversions.h" #include "ui/gtk/gtk_color_mixers.h" #include "ui/gtk/gtk_compat.h" #include "ui/gtk/gtk_util.h" #include "ui/native_theme/common_theme.h" #include "ui/native_theme/native_theme_aura.h" #include "ui/native_theme/native_theme_utils.h" using base::StrCat; namespace gtk { namespace { enum BackgroundRenderMode { BG_RENDER_NORMAL, BG_RENDER_NONE, BG_RENDER_RECURSIVE, }; SkBitmap GetWidgetBitmap(const gfx::Size& size, GtkCssContext context, BackgroundRenderMode bg_mode, bool render_frame) { DCHECK(bg_mode != BG_RENDER_NONE || render_frame); SkBitmap bitmap; bitmap.allocN32Pixels(size.width(), size.height()); bitmap.eraseColor(0); CairoSurface surface(bitmap); cairo_t* cr = surface.cairo(); double opacity = GetOpacityFromContext(context); if (opacity < 1) cairo_push_group(cr); switch (bg_mode) { case BG_RENDER_NORMAL: gtk_render_background(context, cr, 0, 0, size.width(), size.height()); break; case BG_RENDER_RECURSIVE: RenderBackground(size, cr, context); break; case BG_RENDER_NONE: break; } if (render_frame) { gtk_render_frame(context, cr, 0, 0, size.width(), size.height()); } if (opacity < 1) { cairo_pop_group_to_source(cr); cairo_set_operator(cr, CAIRO_OPERATOR_OVER); cairo_paint_with_alpha(cr, opacity); } bitmap.setImmutable(); return bitmap; } void PaintWidget(cc::PaintCanvas* canvas, const gfx::Rect& rect, GtkCssContext context, BackgroundRenderMode bg_mode, bool render_frame) { canvas->drawImage(cc::PaintImage::CreateFromBitmap(GetWidgetBitmap( rect.size(), context, bg_mode, render_frame)), rect.x(), rect.y()); } } // namespace // static NativeThemeGtk* NativeThemeGtk::instance() { static base::NoDestructor s_native_theme; return s_native_theme.get(); } NativeThemeGtk::NativeThemeGtk() : NativeThemeBase(/*should_only_use_dark_colors=*/false, ui::SystemTheme::kGtk) { ui::ColorProviderManager::Get().AppendColorProviderInitializer( base::BindRepeating(AddGtkNativeColorMixer)); OnThemeChanged(gtk_settings_get_default(), nullptr); } NativeThemeGtk::~NativeThemeGtk() { NOTREACHED(); } void NativeThemeGtk::SetThemeCssOverride(ScopedCssProvider provider) { if (theme_css_override_) { if (GtkCheckVersion(4)) { gtk_style_context_remove_provider_for_display( gdk_display_get_default(), GTK_STYLE_PROVIDER(theme_css_override_.get())); } else { gtk_style_context_remove_provider_for_screen( gdk_screen_get_default(), GTK_STYLE_PROVIDER(theme_css_override_.get())); } } theme_css_override_ = std::move(provider); if (theme_css_override_) { if (GtkCheckVersion(4)) { gtk_style_context_add_provider_for_display( gdk_display_get_default(), GTK_STYLE_PROVIDER(theme_css_override_.get()), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); } else { gtk_style_context_add_provider_for_screen( gdk_screen_get_default(), GTK_STYLE_PROVIDER(theme_css_override_.get()), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); } } } void NativeThemeGtk::NotifyOnNativeThemeUpdated() { NativeTheme::NotifyOnNativeThemeUpdated(); // Update the preferred contrast settings for the NativeThemeAura instance and // notify its observers about the change. for (ui::NativeTheme* native_theme : {ui::NativeTheme::GetInstanceForNativeUi(), ui::NativeTheme::GetInstanceForWeb()}) { native_theme->SetPreferredContrast( UserHasContrastPreference() ? ui::NativeThemeBase::PreferredContrast::kMore : ui::NativeThemeBase::PreferredContrast::kNoPreference); native_theme->set_prefers_reduced_transparency(UserHasContrastPreference()); native_theme->NotifyOnNativeThemeUpdated(); } } void NativeThemeGtk::OnThemeChanged(GtkSettings* settings, GtkParamSpec* param) { SetThemeCssOverride(ScopedCssProvider()); std::string theme_name = GetGtkSettingsStringProperty(settings, "gtk-theme-name"); // GTK has a dark mode setting called "gtk-application-prefer-dark-theme", but // this is really only used for themes that have a dark or light variant that // gets toggled based on this setting (eg. Adwaita). Most dark themes do not // have a light variant and aren't affected by the setting. Because of this, // experimentally check if the theme is dark by checking if the window // background color is dark. const SkColor window_bg_color = GetBgColor(""); set_use_dark_colors(IsForcedDarkMode() || color_utils::IsDark(window_bg_color)); set_preferred_color_scheme(CalculatePreferredColorScheme()); // GTK doesn't have a native high contrast setting. Rather, it's implied by // the theme name. The only high contrast GTK themes that I know of are // HighContrast (GNOME) and ContrastHighInverse (MATE). So infer the contrast // based on if the theme name contains both "high" and "contrast", // case-insensitive. base::ranges::transform(theme_name, theme_name.begin(), ::tolower); bool high_contrast = theme_name.find("high") != std::string::npos && theme_name.find("contrast") != std::string::npos; SetPreferredContrast( high_contrast ? ui::NativeThemeBase::PreferredContrast::kMore : ui::NativeThemeBase::PreferredContrast::kNoPreference); NotifyOnNativeThemeUpdated(); } void NativeThemeGtk::PaintMenuPopupBackground( cc::PaintCanvas* canvas, const ui::ColorProvider* color_provider, const gfx::Size& size, const MenuBackgroundExtraParams& menu_background, ColorScheme color_scheme) const { auto context = GetStyleContextFromCss(GtkCssMenu()); // Chrome menus aren't rendered with transparency, so avoid rounded corners. ApplyCssToContext(context, "* { border-radius: 0px; }"); PaintWidget(canvas, gfx::Rect(size), context, BG_RENDER_RECURSIVE, false); } void NativeThemeGtk::PaintMenuItemBackground( cc::PaintCanvas* canvas, const ui::ColorProvider* color_provider, State state, const gfx::Rect& rect, const MenuItemExtraParams& menu_item, ColorScheme color_scheme) const { auto context = GetStyleContextFromCss(StrCat({GtkCssMenu(), " ", GtkCssMenuItem()})); gtk_style_context_set_state(context, StateToStateFlags(state)); PaintWidget(canvas, rect, context, BG_RENDER_NORMAL, true); } void NativeThemeGtk::PaintMenuSeparator( cc::PaintCanvas* canvas, const ui::ColorProvider* color_provider, State state, const gfx::Rect& rect, const MenuSeparatorExtraParams& menu_separator) const { // TODO(estade): use GTK to draw vertical separators too. See // crbug.com/710183 if (menu_separator.type == ui::VERTICAL_SEPARATOR) { cc::PaintFlags paint; paint.setStyle(cc::PaintFlags::kFill_Style); DCHECK(color_provider); paint.setColor(color_provider->GetColor(ui::kColorMenuSeparator)); canvas->drawRect(gfx::RectToSkRect(rect), paint); return; } auto separator_offset = [&](int separator_thickness) { switch (menu_separator.type) { case ui::LOWER_SEPARATOR: return rect.height() - separator_thickness; case ui::UPPER_SEPARATOR: return 0; default: return (rect.height() - separator_thickness) / 2; } }; auto context = GetStyleContextFromCss(StrCat({GtkCssMenu(), " separator.horizontal"})); int min_height = 1; auto margin = GtkStyleContextGetMargin(context); auto border = GtkStyleContextGetBorder(context); auto padding = GtkStyleContextGetPadding(context); if (GtkCheckVersion(4)) { min_height = GetSeparatorSize(true).height(); } else { GtkStyleContextGet(context, "min-height", &min_height, nullptr); } int w = rect.width() - margin.left() - margin.right(); int h = std::max(min_height + padding.top() + padding.bottom() + border.top() + border.bottom(), 1); int x = margin.left(); int y = separator_offset(h); PaintWidget(canvas, gfx::Rect(x, y, w, h), context, BG_RENDER_NORMAL, true); } void NativeThemeGtk::PaintFrameTopArea( cc::PaintCanvas* canvas, State state, const gfx::Rect& rect, const FrameTopAreaExtraParams& frame_top_area, ColorScheme color_scheme) const { auto context = GetStyleContextFromCss(frame_top_area.use_custom_frame ? "headerbar.header-bar.titlebar" : "menubar"); ApplyCssToContext(context, "* { border-radius: 0px; border-style: none; }"); gtk_style_context_set_state(context, frame_top_area.is_active ? GTK_STATE_FLAG_NORMAL : GTK_STATE_FLAG_BACKDROP); SkBitmap bitmap = GetWidgetBitmap( rect.size(), context, frame_top_area.use_custom_frame ? BG_RENDER_NORMAL : BG_RENDER_RECURSIVE, false); bitmap.setImmutable(); canvas->drawImage(cc::PaintImage::CreateFromBitmap(std::move(bitmap)), rect.x(), rect.y()); } } // namespace gtk