123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- // Copyright 2017 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
- #include "ui/gtk/nav_button_provider_gtk.h"
- #include "base/notreached.h"
- #include "ui/base/glib/glib_cast.h"
- #include "ui/base/glib/scoped_gobject.h"
- #include "ui/gfx/image/image_skia.h"
- #include "ui/gfx/image/image_skia_rep.h"
- #include "ui/gfx/image/image_skia_source.h"
- #include "ui/gtk/gtk_compat.h"
- #include "ui/gtk/gtk_util.h"
- #include "ui/linux/nav_button_provider.h"
- #include "ui/views/widget/widget.h"
- namespace gtk {
- namespace {
- struct NavButtonIcon {
- // Used on Gtk3.
- ScopedGObject<GdkPixbuf> pixbuf;
- // Used on Gtk4.
- ScopedGObject<GdkTexture> texture;
- };
- // gtkheaderbar.c uses GTK_ICON_SIZE_MENU, which is 16px.
- const int kNavButtonIconSize = 16;
- // Specified in GtkHeaderBar spec.
- const int kHeaderSpacing = 6;
- const char* ButtonStyleClassFromButtonType(
- ui::NavButtonProvider::FrameButtonDisplayType type) {
- switch (type) {
- case ui::NavButtonProvider::FrameButtonDisplayType::kMinimize:
- return "minimize";
- case ui::NavButtonProvider::FrameButtonDisplayType::kMaximize:
- case ui::NavButtonProvider::FrameButtonDisplayType::kRestore:
- return "maximize";
- case ui::NavButtonProvider::FrameButtonDisplayType::kClose:
- return "close";
- default:
- NOTREACHED();
- return "";
- }
- }
- GtkStateFlags GtkStateFlagsFromButtonState(
- ui::NavButtonProvider::ButtonState state) {
- switch (state) {
- case ui::NavButtonProvider::ButtonState::kNormal:
- return GTK_STATE_FLAG_NORMAL;
- case ui::NavButtonProvider::ButtonState::kHovered:
- return GTK_STATE_FLAG_PRELIGHT;
- case ui::NavButtonProvider::ButtonState::kPressed:
- return static_cast<GtkStateFlags>(GTK_STATE_FLAG_PRELIGHT |
- GTK_STATE_FLAG_ACTIVE);
- case ui::NavButtonProvider::ButtonState::kDisabled:
- return GTK_STATE_FLAG_INSENSITIVE;
- default:
- NOTREACHED();
- return GTK_STATE_FLAG_NORMAL;
- }
- }
- const char* IconNameFromButtonType(
- ui::NavButtonProvider::FrameButtonDisplayType type) {
- switch (type) {
- case ui::NavButtonProvider::FrameButtonDisplayType::kMinimize:
- return "window-minimize-symbolic";
- case ui::NavButtonProvider::FrameButtonDisplayType::kMaximize:
- return "window-maximize-symbolic";
- case ui::NavButtonProvider::FrameButtonDisplayType::kRestore:
- return "window-restore-symbolic";
- case ui::NavButtonProvider::FrameButtonDisplayType::kClose:
- return "window-close-symbolic";
- default:
- NOTREACHED();
- return "";
- }
- }
- gfx::Size LoadNavButtonIcon(ui::NavButtonProvider::FrameButtonDisplayType type,
- GtkStyleContext* button_context,
- int scale,
- NavButtonIcon* icon = nullptr) {
- const char* icon_name = IconNameFromButtonType(type);
- if (!GtkCheckVersion(4)) {
- auto icon_info = TakeGObject(gtk_icon_theme_lookup_icon_for_scale(
- GetDefaultIconTheme(), icon_name, kNavButtonIconSize, scale,
- static_cast<GtkIconLookupFlags>(GTK_ICON_LOOKUP_USE_BUILTIN |
- GTK_ICON_LOOKUP_GENERIC_FALLBACK)));
- auto icon_pixbuf = TakeGObject(gtk_icon_info_load_symbolic_for_context(
- icon_info, button_context, nullptr, nullptr));
- gfx::Size size{gdk_pixbuf_get_width(icon_pixbuf),
- gdk_pixbuf_get_height(icon_pixbuf)};
- if (icon)
- icon->pixbuf = std::move(icon_pixbuf);
- return size;
- }
- auto icon_paintable = Gtk4IconThemeLookupIcon(
- GetDefaultIconTheme(), icon_name, nullptr, kNavButtonIconSize, scale,
- GTK_TEXT_DIR_NONE, static_cast<GtkIconLookupFlags>(0));
- auto* paintable =
- GlibCast<GdkPaintable>(icon_paintable.get(), gdk_paintable_get_type());
- int width = scale * gdk_paintable_get_intrinsic_width(paintable);
- int height = scale * gdk_paintable_get_intrinsic_height(paintable);
- if (icon) {
- auto* snapshot = gtk_snapshot_new();
- gdk_paintable_snapshot(paintable, snapshot, width, height);
- auto* node = gtk_snapshot_free_to_node(snapshot);
- GdkTexture* texture = GetTextureFromRenderNode(node);
- size_t nbytes = width * height * sizeof(SkColor);
- SkColor* pixels = reinterpret_cast<SkColor*>(g_malloc(nbytes));
- size_t stride = sizeof(SkColor) * width;
- gdk_texture_download(texture, reinterpret_cast<guchar*>(pixels), stride);
- SkColor fg = GtkStyleContextGetColor(button_context);
- for (int i = 0; i < width * height; ++i)
- pixels[i] = SkColorSetA(fg, SkColorGetA(pixels[i]));
- icon->texture = TakeGObject(
- gdk_memory_texture_new(width, height, GDK_MEMORY_B8G8R8A8,
- g_bytes_new_take(pixels, nbytes), stride));
- gsk_render_node_unref(node);
- }
- return {width, height};
- }
- gfx::Size GetMinimumWidgetSize(gfx::Size content_size,
- GtkStyleContext* content_context,
- GtkCssContext widget_context) {
- gfx::Rect widget_rect = gfx::Rect(content_size);
- if (content_context)
- widget_rect.Inset(-GtkStyleContextGetMargin(content_context));
- int min_width = 0;
- int min_height = 0;
- // On GTK3, get the min size from the CSS directly.
- if (GtkCheckVersion(3, 20) && !GtkCheckVersion(4)) {
- GtkStyleContextGet(widget_context, "min-width", &min_width, "min-height",
- &min_height, nullptr);
- widget_rect.set_width(std::max(widget_rect.width(), min_width));
- widget_rect.set_height(std::max(widget_rect.height(), min_height));
- }
- widget_rect.Inset(-GtkStyleContextGetPadding(widget_context));
- widget_rect.Inset(-GtkStyleContextGetBorder(widget_context));
- // On GTK4, the CSS properties are hidden, so compute the min size indirectly,
- // which will include the border, margin, and padding. We can't take this
- // codepath on GTK3 since we only have a widget available in GTK4.
- if (GtkCheckVersion(4)) {
- gtk_widget_measure(widget_context.widget(), GTK_ORIENTATION_HORIZONTAL, -1,
- &min_width, nullptr, nullptr, nullptr);
- gtk_widget_measure(widget_context.widget(), GTK_ORIENTATION_VERTICAL, -1,
- &min_height, nullptr, nullptr, nullptr);
- // The returned "minimum size" is the drawn size of the widget, which
- // doesn't include the margin. However, GTK includes this size in its
- // calculation. So remove the margin, recompute the min size, then add it
- // back.
- auto margin = GtkStyleContextGetMargin(widget_context);
- widget_rect.Inset(-margin);
- widget_rect.set_width(std::max(widget_rect.width(), min_width));
- widget_rect.set_height(std::max(widget_rect.height(), min_height));
- widget_rect.Inset(margin);
- }
- return widget_rect.size();
- }
- GtkCssContext CreateHeaderContext(bool maximized) {
- std::string window_selector = "GtkWindow#window.background.csd";
- if (maximized)
- window_selector += ".maximized";
- return AppendCssNodeToStyleContext(
- AppendCssNodeToStyleContext({}, window_selector),
- "GtkHeaderBar#headerbar.header-bar.titlebar");
- }
- GtkCssContext CreateWindowControlsContext(bool maximized) {
- return AppendCssNodeToStyleContext(CreateHeaderContext(maximized),
- "#windowcontrols");
- }
- void CalculateUnscaledButtonSize(
- ui::NavButtonProvider::FrameButtonDisplayType type,
- bool maximized,
- gfx::Size* button_size,
- gfx::Insets* button_margin) {
- // views::ImageButton expects the images for each state to be of the
- // same size, but GTK can, in general, use a differnetly-sized
- // button for each state. For this reason, render buttons for all
- // states at the size of a GTK_STATE_FLAG_NORMAL button.
- auto button_context = AppendCssNodeToStyleContext(
- CreateWindowControlsContext(maximized),
- "GtkButton#button.titlebutton." +
- std::string(ButtonStyleClassFromButtonType(type)));
- auto icon_size = LoadNavButtonIcon(type, button_context, 1);
- auto image_context =
- AppendCssNodeToStyleContext(button_context, "GtkImage#image");
- gfx::Size image_size =
- GetMinimumWidgetSize(icon_size, nullptr, image_context);
- *button_size =
- GetMinimumWidgetSize(image_size, image_context, button_context);
- *button_margin = GtkStyleContextGetMargin(button_context);
- }
- class NavButtonImageSource : public gfx::ImageSkiaSource {
- public:
- NavButtonImageSource(ui::NavButtonProvider::FrameButtonDisplayType type,
- ui::NavButtonProvider::ButtonState state,
- bool maximized,
- bool active,
- gfx::Size button_size)
- : type_(type),
- state_(state),
- maximized_(maximized),
- active_(active),
- button_size_(button_size) {}
- ~NavButtonImageSource() override = default;
- gfx::ImageSkiaRep GetImageForScale(float scale) override {
- // gfx::ImageSkia kindly caches the result of this function, so
- // RenderNavButton() is called at most once for each needed scale
- // factor. Additionally, buttons in the HOVERED or PRESSED states
- // are not actually rendered until they are needed.
- if (button_size_.IsEmpty())
- return gfx::ImageSkiaRep();
- auto button_context =
- AppendCssNodeToStyleContext(CreateWindowControlsContext(maximized_),
- "GtkButton#button.titlebutton");
- gtk_style_context_add_class(button_context,
- ButtonStyleClassFromButtonType(type_));
- GtkStateFlags button_state = GtkStateFlagsFromButtonState(state_);
- if (!active_) {
- button_state =
- static_cast<GtkStateFlags>(button_state | GTK_STATE_FLAG_BACKDROP);
- }
- gtk_style_context_set_state(button_context, button_state);
- // Gtk header bars usually have the same height in both maximized and
- // restored windows. But chrome's tabstrip background has a smaller height
- // when maximized. To prevent buttons from clipping outside of this region,
- // they are scaled down. However, this is problematic for themes that do
- // not expect this case and use bitmaps for frame buttons (like the Breeze
- // theme). When the background-size is set to auto, the background bitmap
- // is not scaled for the (unexpected) smaller button size, and the button's
- // edges appear cut off. To fix this, manually set the background to scale
- // to the button size when it would have clipped.
- //
- // GTK's "contain" is unlike CSS's "contain". In CSS, the image would only
- // be downsized when it would have clipped. In GTK, the image is always
- // scaled to fit the drawing region (preserving aspect ratio). Only add
- // "contain" if clipping would occur.
- int bg_width = 0;
- int bg_height = 0;
- if (GtkCheckVersion(4)) {
- auto* snapshot = gtk_snapshot_new();
- gtk_snapshot_render_background(snapshot, button_context, 0, 0,
- button_size_.width(),
- button_size_.height());
- if (auto* node = gtk_snapshot_free_to_node(snapshot)) {
- if (GdkTexture* texture = GetTextureFromRenderNode(node)) {
- bg_width = gdk_texture_get_width(texture);
- bg_height = gdk_texture_get_height(texture);
- }
- gsk_render_node_unref(node);
- }
- } else {
- cairo_pattern_t* cr_pattern = nullptr;
- cairo_surface_t* cr_surface = nullptr;
- GtkStyleContextGet(
- button_context,
- "background-image" /* GTK_STYLE_PROPERTY_BACKGROUND_IMAGE */,
- &cr_pattern, nullptr);
- if (cr_pattern) {
- cairo_pattern_get_surface(cr_pattern, &cr_surface);
- if (cr_surface &&
- cairo_surface_get_type(cr_surface) == CAIRO_SURFACE_TYPE_IMAGE) {
- bg_width = cairo_image_surface_get_width(cr_surface);
- bg_height = cairo_image_surface_get_height(cr_surface);
- }
- cairo_pattern_destroy(cr_pattern);
- }
- }
- if (bg_width > button_size_.width() || bg_height > button_size_.height()) {
- ApplyCssToContext(button_context,
- ".titlebutton { background-size: contain; }");
- }
- // Gtk doesn't support fractional scale factors, but chrome does.
- // Rendering the button background and border at a fractional
- // scale factor is easy, since we can adjust the cairo context
- // transform. But the icon is loaded from a pixbuf, so we pick
- // the next-highest integer scale and manually downsize.
- int pixbuf_scale = scale == static_cast<int>(scale) ? scale : scale + 1;
- NavButtonIcon icon;
- auto icon_size =
- LoadNavButtonIcon(type_, button_context, pixbuf_scale, &icon);
- SkBitmap bitmap;
- bitmap.allocN32Pixels(scale * button_size_.width(),
- scale * button_size_.height());
- bitmap.eraseColor(0);
- CairoSurface surface(bitmap);
- cairo_t* cr = surface.cairo();
- cairo_save(cr);
- cairo_scale(cr, scale, scale);
- if (GtkCheckVersion(3, 11, 3) ||
- (button_state & (GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_ACTIVE))) {
- gtk_render_background(button_context, cr, 0, 0, button_size_.width(),
- button_size_.height());
- gtk_render_frame(button_context, cr, 0, 0, button_size_.width(),
- button_size_.height());
- }
- cairo_restore(cr);
- cairo_save(cr);
- float pixbuf_extra_scale = scale / pixbuf_scale;
- cairo_scale(cr, pixbuf_extra_scale, pixbuf_extra_scale);
- GtkRenderIcon(
- button_context, cr, icon.pixbuf, icon.texture,
- ((pixbuf_scale * button_size_.width() - icon_size.width()) / 2),
- ((pixbuf_scale * button_size_.height() - icon_size.height()) / 2));
- cairo_restore(cr);
- return gfx::ImageSkiaRep(bitmap, scale);
- }
- bool HasRepresentationAtAllScales() const override { return true; }
- private:
- ui::NavButtonProvider::FrameButtonDisplayType type_;
- ui::NavButtonProvider::ButtonState state_;
- bool maximized_;
- bool active_;
- gfx::Size button_size_;
- };
- } // namespace
- NavButtonProviderGtk::NavButtonProviderGtk() = default;
- NavButtonProviderGtk::~NavButtonProviderGtk() = default;
- void NavButtonProviderGtk::RedrawImages(int top_area_height,
- bool maximized,
- bool active) {
- auto header_context = CreateHeaderContext(maximized);
- auto header_padding = GtkStyleContextGetPadding(header_context);
- double scale = 1.0f;
- std::map<ui::NavButtonProvider::FrameButtonDisplayType, gfx::Size>
- button_sizes;
- std::map<ui::NavButtonProvider::FrameButtonDisplayType, gfx::Insets>
- button_margins;
- std::vector<ui::NavButtonProvider::FrameButtonDisplayType> display_types{
- ui::NavButtonProvider::FrameButtonDisplayType::kMinimize,
- maximized ? ui::NavButtonProvider::FrameButtonDisplayType::kRestore
- : ui::NavButtonProvider::FrameButtonDisplayType::kMaximize,
- ui::NavButtonProvider::FrameButtonDisplayType::kClose,
- };
- for (auto type : display_types) {
- CalculateUnscaledButtonSize(type, maximized, &button_sizes[type],
- &button_margins[type]);
- int button_unconstrained_height = button_sizes[type].height() +
- button_margins[type].top() +
- button_margins[type].bottom();
- int needed_height = header_padding.top() + button_unconstrained_height +
- header_padding.bottom();
- if (needed_height > top_area_height)
- scale =
- std::min(scale, static_cast<double>(top_area_height) / needed_height);
- }
- top_area_spacing_ =
- gfx::Insets::TLBR(std::round(scale * header_padding.top()),
- std::round(scale * header_padding.left()),
- std::round(scale * header_padding.bottom()),
- std::round(scale * header_padding.right()));
- inter_button_spacing_ = std::round(scale * kHeaderSpacing);
- for (auto type : display_types) {
- double button_height =
- scale * (button_sizes[type].height() + button_margins[type].top() +
- button_margins[type].bottom());
- double available_height =
- top_area_height -
- scale * (header_padding.top() + header_padding.bottom());
- double scaled_button_offset = (available_height - button_height) / 2;
- gfx::Size size = button_sizes[type];
- size = gfx::Size(std::round(scale * size.width()),
- std::round(scale * size.height()));
- gfx::Insets margin = button_margins[type];
- margin = gfx::Insets::TLBR(
- std::round(scale * (header_padding.top() + margin.top()) +
- scaled_button_offset),
- std::round(scale * margin.left()), 0,
- std::round(scale * margin.right()));
- button_margins_[type] = margin;
- for (auto state : {
- ui::NavButtonProvider::ButtonState::kNormal,
- ui::NavButtonProvider::ButtonState::kHovered,
- ui::NavButtonProvider::ButtonState::kPressed,
- ui::NavButtonProvider::ButtonState::kDisabled,
- }) {
- button_images_[type][state] =
- gfx::ImageSkia(std::make_unique<NavButtonImageSource>(
- type, state, maximized, active, size),
- size);
- }
- }
- }
- gfx::ImageSkia NavButtonProviderGtk::GetImage(
- ui::NavButtonProvider::FrameButtonDisplayType type,
- ui::NavButtonProvider::ButtonState state) const {
- auto it = button_images_.find(type);
- DCHECK(it != button_images_.end());
- auto it2 = it->second.find(state);
- DCHECK(it2 != it->second.end());
- return it2->second;
- }
- gfx::Insets NavButtonProviderGtk::GetNavButtonMargin(
- ui::NavButtonProvider::FrameButtonDisplayType type) const {
- auto it = button_margins_.find(type);
- DCHECK(it != button_margins_.end());
- return it->second;
- }
- gfx::Insets NavButtonProviderGtk::GetTopAreaSpacing() const {
- return top_area_spacing_;
- }
- int NavButtonProviderGtk::GetInterNavButtonSpacing() const {
- return inter_button_spacing_;
- }
- } // namespace gtk
|