| // 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<NativeThemeGtk> 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 |