blob: 5aa603701e07c017c1e1495d826931feffa65169 [file] [log] [blame]
// 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 "chrome/browser/ui/libgtkui/nav_button_provider_gtk3.h"
#include <gtk/gtk.h>
#include "chrome/browser/ui/libgtkui/gtk_util.h"
#include "ui/base/glib/scoped_gobject.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_source.h"
#include "ui/views/resources/grit/views_resources.h"
namespace libgtkui {
namespace {
// gtkheaderbar.c uses GTK_ICON_SIZE_MENU, which is 16px.
const int kIconSize = 16;
// Specified in GtkHeaderBar spec.
const int kHeaderSpacing = 6;
const char* ButtonStyleClassFromButtonType(
chrome::FrameButtonDisplayType type) {
switch (type) {
case chrome::FrameButtonDisplayType::kMinimize:
return "minimize";
case chrome::FrameButtonDisplayType::kMaximize:
return "maximize";
case chrome::FrameButtonDisplayType::kRestore:
return "restore";
case chrome::FrameButtonDisplayType::kClose:
return "close";
default:
NOTREACHED();
return "";
}
}
GtkStateFlags GtkStateFlagsFromButtonState(views::Button::ButtonState state) {
switch (state) {
case views::Button::STATE_NORMAL:
return GTK_STATE_FLAG_NORMAL;
case views::Button::STATE_HOVERED:
return GTK_STATE_FLAG_PRELIGHT;
case views::Button::STATE_PRESSED:
return static_cast<GtkStateFlags>(GTK_STATE_FLAG_PRELIGHT |
GTK_STATE_FLAG_ACTIVE);
case views::Button::STATE_DISABLED:
return GTK_STATE_FLAG_INSENSITIVE;
default:
NOTREACHED();
return GTK_STATE_FLAG_NORMAL;
}
}
const char* IconNameFromButtonType(chrome::FrameButtonDisplayType type) {
switch (type) {
case chrome::FrameButtonDisplayType::kMinimize:
return "window-minimize-symbolic";
case chrome::FrameButtonDisplayType::kMaximize:
return "window-maximize-symbolic";
case chrome::FrameButtonDisplayType::kRestore:
return "window-restore-symbolic";
case chrome::FrameButtonDisplayType::kClose:
return "window-close-symbolic";
default:
NOTREACHED();
return "";
}
}
gfx::Insets InsetsFromGtkBorder(const GtkBorder& border) {
return gfx::Insets(border.top, border.left, border.bottom, border.right);
}
gfx::Insets PaddingFromStyleContext(GtkStyleContext* context,
GtkStateFlags state) {
GtkBorder padding;
gtk_style_context_get_padding(context, state, &padding);
return InsetsFromGtkBorder(padding);
}
gfx::Insets BorderFromStyleContext(GtkStyleContext* context,
GtkStateFlags state) {
GtkBorder border;
gtk_style_context_get_border(context, state, &border);
return InsetsFromGtkBorder(border);
}
gfx::Insets MarginFromStyleContext(GtkStyleContext* context,
GtkStateFlags state) {
GtkBorder margin;
gtk_style_context_get_margin(context, state, &margin);
return InsetsFromGtkBorder(margin);
}
int DefaultMonitorScaleFactor() {
GdkScreen* screen = gdk_screen_get_default();
return gdk_screen_get_monitor_scale_factor(
screen, gdk_screen_get_primary_monitor(screen));
}
ScopedGObject<GdkPixbuf> LoadNavButtonIcon(chrome::FrameButtonDisplayType type,
GtkStyleContext* button_context,
int scale) {
const char* icon_name = IconNameFromButtonType(type);
ScopedGObject<GtkIconInfo> icon_info(gtk_icon_theme_lookup_icon_for_scale(
gtk_icon_theme_get_default(), icon_name, kIconSize, scale,
static_cast<GtkIconLookupFlags>(GTK_ICON_LOOKUP_USE_BUILTIN |
GTK_ICON_LOOKUP_GENERIC_FALLBACK)));
return ScopedGObject<GdkPixbuf>(gtk_icon_info_load_symbolic_for_context(
icon_info, button_context, nullptr, nullptr));
}
void CalculateUnscaledButtonSize(chrome::FrameButtonDisplayType type,
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 = GetStyleContextFromCss(
"GtkHeaderBar#headerbar.header-bar.titlebar "
"GtkButton#button.titlebutton");
gtk_style_context_add_class(button_context,
ButtonStyleClassFromButtonType(type));
ScopedGObject<GdkPixbuf> icon_pixbuf =
LoadNavButtonIcon(type, button_context, 1);
const int monitor_scale = DefaultMonitorScaleFactor();
gfx::Size icon_size(gdk_pixbuf_get_width(icon_pixbuf) / monitor_scale,
gdk_pixbuf_get_height(icon_pixbuf) / monitor_scale);
gfx::Rect button_rect(icon_size);
if (GtkVersionCheck(3, 20)) {
int min_width, min_height;
gtk_style_context_get(button_context, GTK_STATE_FLAG_NORMAL, "min-width",
&min_width, "min-height", &min_height, NULL);
button_rect.set_width(std::max(button_rect.width(), min_width));
button_rect.set_height(std::max(button_rect.height(), min_height));
}
// TODO(thomasanderson): Factor in the GtkImage size, border, padding, and
// margin
button_rect.Inset(
-PaddingFromStyleContext(button_context, GTK_STATE_FLAG_NORMAL));
button_rect.Inset(
-BorderFromStyleContext(button_context, GTK_STATE_FLAG_NORMAL));
*button_size = button_rect.size();
*button_margin =
MarginFromStyleContext(button_context, GTK_STATE_FLAG_NORMAL);
}
class NavButtonImageSource : public gfx::ImageSkiaSource {
public:
NavButtonImageSource(chrome::FrameButtonDisplayType type,
views::Button::ButtonState state,
bool active,
gfx::Size button_size)
: type_(type),
state_(state),
active_(active),
button_size_(button_size) {}
~NavButtonImageSource() override {}
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.
auto button_context = GetStyleContextFromCss(
"GtkHeaderBar#headerbar.header-bar.titlebar "
"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 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;
ScopedGObject<GdkPixbuf> icon_pixbuf =
LoadNavButtonIcon(type_, button_context, pixbuf_scale);
gfx::Size icon_size(gdk_pixbuf_get_width(icon_pixbuf),
gdk_pixbuf_get_height(icon_pixbuf));
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 (GtkVersionCheck(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);
gtk_render_icon(
button_context, cr, icon_pixbuf,
((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:
chrome::FrameButtonDisplayType type_;
views::Button::ButtonState state_;
bool active_;
gfx::Size button_size_;
};
} // namespace
NavButtonProviderGtk3::NavButtonProviderGtk3() {}
NavButtonProviderGtk3::~NavButtonProviderGtk3() {}
void NavButtonProviderGtk3::RedrawImages(int top_area_height,
bool maximized,
bool active) {
auto header_context =
GetStyleContextFromCss("GtkHeaderBar#headerbar.header-bar.titlebar");
GtkBorder header_padding;
gtk_style_context_get_padding(header_context, GTK_STATE_FLAG_NORMAL,
&header_padding);
GtkBorder header_border;
gtk_style_context_get_border(header_context, GTK_STATE_FLAG_NORMAL,
&header_border);
double scale = 1.0f;
std::map<chrome::FrameButtonDisplayType, gfx::Size> button_sizes;
std::map<chrome::FrameButtonDisplayType, gfx::Insets> button_margins;
std::vector<chrome::FrameButtonDisplayType> display_types{
chrome::FrameButtonDisplayType::kMinimize,
maximized ? chrome::FrameButtonDisplayType::kRestore
: chrome::FrameButtonDisplayType::kMaximize,
chrome::FrameButtonDisplayType::kClose,
};
for (auto type : display_types) {
CalculateUnscaledButtonSize(type, &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_border.top + header_padding.top +
button_unconstrained_height + header_padding.bottom +
header_border.bottom;
if (needed_height > top_area_height)
scale =
std::min(scale, static_cast<double>(top_area_height) / needed_height);
}
top_area_spacing_ =
InsetsFromGtkBorder(header_padding) + InsetsFromGtkBorder(header_border);
top_area_spacing_ = gfx::Insets(scale * top_area_spacing_.top() + 0.5f,
scale * top_area_spacing_.left() + 0.5f,
scale * top_area_spacing_.bottom() + 0.5f,
scale * top_area_spacing_.right() + 0.5f);
inter_button_spacing_ = scale * kHeaderSpacing + 0.5f;
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 +
header_border.top + header_border.bottom));
double scaled_button_offset = (available_height - button_height) / 2;
gfx::Size size = button_sizes[type];
size = gfx::Size(scale * size.width() + 0.5f, scale * size.height() + 0.5f);
gfx::Insets margin = button_margins[type];
margin = gfx::Insets(
scale * (header_border.top + header_padding.top + margin.top()) +
scaled_button_offset + 0.5f,
0, scale * margin.left() + 0.5f, scale * margin.right() + 0.5f);
button_margins_[type] = margin;
for (size_t state = 0; state < views::Button::STATE_COUNT; state++) {
button_images_[type][state] = gfx::ImageSkia(
std::make_unique<NavButtonImageSource>(
type, static_cast<views::Button::ButtonState>(state), active,
size),
size);
}
}
}
gfx::ImageSkia NavButtonProviderGtk3::GetImage(
chrome::FrameButtonDisplayType type,
views::Button::ButtonState state) const {
auto it = button_images_.find(type);
DCHECK(it != button_images_.end());
return it->second[state];
}
gfx::Insets NavButtonProviderGtk3::GetNavButtonMargin(
chrome::FrameButtonDisplayType type) const {
auto it = button_margins_.find(type);
DCHECK(it != button_margins_.end());
return it->second;
}
gfx::Insets NavButtonProviderGtk3::GetTopAreaSpacing() const {
return top_area_spacing_;
}
int NavButtonProviderGtk3::GetInterNavButtonSpacing() const {
return inter_button_spacing_;
}
} // namespace libgtkui